Compare commits

..

No commits in common. "main" and "feat/api-gap-fill" have entirely different histories.

85 changed files with 1202 additions and 8520 deletions

View File

@ -2,11 +2,11 @@
## Summary
The MCGhidra Docker container fails to start the HTTP API server because `MCGhidraServer.java` imports Gson, but Gson is not available in Ghidra's headless script classpath.
The GhydraMCP Docker container fails to start the HTTP API server because `GhydraMCPServer.java` imports Gson, but Gson is not available in Ghidra's headless script classpath.
## Environment
- MCGhidra Docker image: `mcghidra:latest`
- GhydraMCP Docker image: `ghydramcp:latest`
- Ghidra Version: 11.4.2
- Build Date: 2025-08-26
@ -14,12 +14,12 @@ The MCGhidra Docker container fails to start the HTTP API server because `MCGhid
1. Build the Docker image:
```bash
docker build -t mcghidra:latest -f docker/Dockerfile .
docker build -t ghydramcp:latest -f docker/Dockerfile .
```
2. Run with a binary:
```bash
docker run -p 8192:8192 -v /path/to/binary:/binaries/test mcghidra:latest /binaries/test
docker run -p 8192:8192 -v /path/to/binary:/binaries/test ghydramcp:latest /binaries/test
```
3. Check logs:
@ -37,9 +37,9 @@ Analysis completes but the script fails to load:
```
INFO REPORT: Analysis succeeded for file: file:///binaries/cardv (HeadlessAnalyzer)
ERROR REPORT SCRIPT ERROR: MCGhidraServer.java : The class could not be found.
ERROR REPORT SCRIPT ERROR: GhydraMCPServer.java : The class could not be found.
It must be the public class of the .java file: Failed to get OSGi bundle containing script:
/opt/ghidra/scripts/MCGhidraServer.java (HeadlessAnalyzer)
/opt/ghidra/scripts/GhydraMCPServer.java (HeadlessAnalyzer)
```
The health check fails because the HTTP server never starts:
@ -50,7 +50,7 @@ The health check fails because the HTTP server never starts:
## Root Cause Analysis
`MCGhidraServer.java` (lines 22-24) imports Gson:
`GhydraMCPServer.java` (lines 22-24) imports Gson:
```java
import com.google.gson.Gson;
@ -61,14 +61,14 @@ import com.google.gson.JsonParser;
However:
1. Gson is **not** bundled with Ghidra
2. The MCGhidra extension JAR includes Gson, but headless scripts run in a **separate OSGi classloader** without access to extension lib dependencies
2. The GhydraMCP extension JAR includes Gson, but headless scripts run in a **separate OSGi classloader** without access to extension lib dependencies
3. The Dockerfile doesn't copy Gson to Ghidra's script classpath
## Verification
```bash
# Check if Gson is in the built extension
unzip -l target/MCGhidra-*.zip | grep -i gson
unzip -l target/GhydraMCP-*.zip | grep -i gson
# Result: No matches
# Check Ghidra's lib directories
@ -90,13 +90,13 @@ RUN curl -fsSL "https://repo1.maven.org/maven2/com/google/gson/gson/2.10.1/gson-
### Option 2: Use Built-in JSON (No External Dependencies)
Rewrite `MCGhidraServer.java` to use only JDK classes:
Rewrite `GhydraMCPServer.java` to use only JDK classes:
- Replace Gson with `javax.json` or manual JSON string building
- This ensures the script works without any external dependencies
### Option 3: Pre-compiled Script JAR
Compile `MCGhidraServer.java` with Gson into a JAR and place it in the extension, then reference it differently in headless mode.
Compile `GhydraMCPServer.java` with Gson into a JAR and place it in the extension, then reference it differently in headless mode.
## Impact
@ -106,7 +106,7 @@ Compile `MCGhidraServer.java` with Gson into a JAR and place it in the extension
## Additional Context
The main MCGhidra plugin works fine in GUI mode because the extension's lib dependencies are loaded. This only affects the headless Docker workflow where scripts are loaded separately from the extension.
The main GhydraMCP plugin works fine in GUI mode because the extension's lib dependencies are loaded. This only affects the headless Docker workflow where scripts are loaded separately from the extension.
---

View File

@ -7,30 +7,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- **Symbol CRUD Operations:** Full create/rename/delete support for symbols and labels:
- `symbols_create` - Create new label/symbol at an address
- `symbols_rename` - Rename existing symbol
- `symbols_delete` - Delete symbol at an address
- `symbols_imports` - List imported symbols with pagination
- `symbols_exports` - List exported symbols with pagination
- **Bookmark Management:** Tools for managing Ghidra bookmarks:
- `bookmarks_list` - List bookmarks with type/category filtering
- `bookmarks_create` - Create bookmark at address (Note, Warning, Error, Info types)
- `bookmarks_delete` - Delete bookmarks at an address
- **Enum & Typedef Creation:** Data type creation tools:
- `enums_create` - Create new enum data type
- `enums_list` - List enum types with members
- `typedefs_create` - Create new typedef
- `typedefs_list` - List typedef data types
- **Variable Management:** Enhanced variable operations:
- `variables_list` - List variables with global_only filter
- `variables_rename` - Rename and retype function variables
- `functions_variables` - List local variables and parameters for a function
- **Namespace & Class Tools:**
- `namespaces_list` - List all non-global namespaces
- `classes_list` - List class namespaces with qualified names
- **Memory Segment Tools:**
- `segments_list` - List memory segments with R/W/X permissions and size info
- **Progress Reporting for Long Operations:** 7 MCP prompts now report real-time progress during multi-step scanning operations:
- `malware_triage` - Reports progress across 21 scanning steps
- `analyze_imports` - Reports progress across 12 capability categories
@ -57,33 +33,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `document_struct` - Comprehensively document data structure fields and usage
- `find_error_handlers` - Map error handling, cleanup routines, and exit paths
### Changed
- **Docker Port Allocation:** Ports are now auto-allocated from pool (8192-8223) instead of client-specified. Prevents session collisions in multi-agent environments.
- **docker_auto_start:** Removed `wait` and `timeout` parameters. Always returns immediately after starting container.
- **Removed docker_wait tool:** This tool blocked for up to 5 minutes in a single call. LLMs should poll `docker_health(port)` in their own loop instead — this gives visibility into progress and ability to check logs between polls.
### Fixed
- **instances_use Hanging:** Eliminated 4+ hour hangs by removing blocking HTTP call. Now uses lazy registration — just creates a stub entry, validates on first real tool call.
- **All Docker Operations Non-Blocking:** ALL Docker subprocess calls (`docker ps`, `docker run`, `docker stop`, etc.) now run in thread executor via `run_in_executor()`. Previously only `docker_health` was fixed, but `docker_status`, `docker_start`, `docker_stop`, `docker_logs`, `docker_build`, and `docker_cleanup` still blocked the event loop. This caused `docker_auto_start(wait=True)` to freeze the MCP server.
- **Session Isolation:** `docker_stop` now validates container belongs to current session before stopping. `docker_cleanup` defaults to `session_only=True` to prevent cross-session interference.
- **Background Discovery Thread:** Fixed timeout from 30s to 0.5s for port scanning, reducing discovery cycle from 300s+ to ~15s.
- **Typedef/Variable Type Resolution:** Fixed `handle_typedef_create` and `handle_variable_rename` to use shared `resolve_data_type()` for builtin types (int, char, etc.).
- **DockerMixin Inheritance:** Fixed crash when `DockerMixin` called `get_instance_port()` — was inheriting from wrong base class.
- **Deprecated asyncio API:** Replaced `asyncio.get_event_loop()` with `asyncio.get_running_loop()` for Python 3.10+ compatibility.
- **HTTP Client Data Mutation:** `safe_post`, `safe_put`, and `safe_patch` no longer mutate the caller's data dict via `.pop()`.
- **Race Condition in Discovery:** Initial instance discovery in `main()` now uses `_instances_lock` for thread safety.
- **Silent Exception Handling:** Added debug logging to PortPool exception handlers and analysis fallback paths.
- **File Descriptor Leak:** Fixed potential leak in `PortPool._try_acquire_port()` if write operations fail after lock acquisition.
- **Hash Algorithm Consistency:** Changed query hash from MD5 to SHA-256 in pagination module for consistency with cursor ID generation.
- **Lazy PortPool Initialization:** `PortPool` now created on first use, avoiding `/tmp/mcghidra-ports` directory creation when Docker tools are never used.
- **Logging Configuration:** `configure_logging()` now called during server startup — debug messages actually work now.
- **Type Hint Consistency:** Aligned `filtering.py` to use `List[T]` from typing module like rest of codebase.
- **Parameter Naming:** Renamed `project_fields` to `fields` in `structs_get()` for consistency with other tools.
- **Import Path:** Fixed `logging.py` to import `Context` from `fastmcp` (not deprecated `mcp.server.fastmcp` path).
### Added
- **Debug Logging Environment Variable:** Set `MCGHIDRA_DEBUG=1` to enable DEBUG-level logging for troubleshooting.
## [2025.12.1] - 2025-12-01
### Added
@ -239,7 +188,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [1.1] - 2025-03-30
### Added
- Initial release of MCGhidra bridge
- Initial release of GhydraMCP bridge
- Basic Ghidra instance management tools
- Function analysis tools
- Variable manipulation tools
@ -250,11 +199,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Initial project setup
- Basic MCP bridge functionality
[unreleased]: https://github.com/teal-bauer/MCGhidra/compare/v2025.12.1...HEAD
[2025.12.1]: https://github.com/teal-bauer/MCGhidra/compare/v2.0.0...v2025.12.1
[2.0.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.4.0...v2.0.0
[1.4.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.2...v1.3.0
[1.2]: https://github.com/teal-bauer/MCGhidra/compare/v1.1...v1.2
[1.1]: https://github.com/teal-bauer/MCGhidra/compare/1.0...v1.1
[1.0]: https://github.com/teal-bauer/MCGhidra/releases/tag/1.0
[unreleased]: https://github.com/teal-bauer/GhydraMCP/compare/v2025.12.1...HEAD
[2025.12.1]: https://github.com/teal-bauer/GhydraMCP/compare/v2.0.0...v2025.12.1
[2.0.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.4.0...v2.0.0
[1.4.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.2...v1.3.0
[1.2]: https://github.com/teal-bauer/GhydraMCP/compare/v1.1...v1.2
[1.1]: https://github.com/teal-bauer/GhydraMCP/compare/1.0...v1.1
[1.0]: https://github.com/teal-bauer/GhydraMCP/releases/tag/1.0

View File

@ -1,6 +1,6 @@
# Contributing to MCGhidra
# Contributing to GhydraMCP
Thank you for your interest in contributing to MCGhidra! This document provides guidelines and information for contributors.
Thank you for your interest in contributing to GhydraMCP! This document provides guidelines and information for contributors.
## Table of Contents
@ -13,10 +13,10 @@ Thank you for your interest in contributing to MCGhidra! This document provides
## Project Structure
MCGhidra consists of two main components:
GhydraMCP consists of two main components:
1. **Java Plugin for Ghidra** (`src/main/java/eu/starsong/ghidra/`):
- Main class: `MCGhidraPlugin.java`
- Main class: `GhydraMCPPlugin.java`
- API constants: `api/ApiConstants.java`
- Endpoints: `endpoints/` directory
- Data models: `model/` directory
@ -39,23 +39,23 @@ MCGhidra consists of two main components:
```bash
# Clone the repository
git clone https://github.com/starsong-consulting/MCGhidra.git
cd MCGhidra
git clone https://github.com/starsong-consulting/GhydraMCP.git
cd GhydraMCP
# Build the project
mvn clean package
```
This creates:
- `target/MCGhidra-[version].zip` - The Ghidra plugin only
- `target/MCGhidra-Complete-[version].zip` - Complete package with plugin and bridge script
- `target/GhydraMCP-[version].zip` - The Ghidra plugin only
- `target/GhydraMCP-Complete-[version].zip` - Complete package with plugin and bridge script
### Installing for Development
1. Build the project as described above
2. In Ghidra, go to `File` -> `Install Extensions`
3. Click the `+` button
4. Select the `MCGhidra-[version].zip` file
4. Select the `GhydraMCP-[version].zip` file
5. Restart Ghidra
6. Enable the plugin in `File` -> `Configure` -> `Developer`
@ -75,7 +75,7 @@ uv pip install mcp==1.6.0 requests==2.32.3
## Versioning
MCGhidra follows semantic versioning (SemVer) and uses explicit API versions:
GhydraMCP follows semantic versioning (SemVer) and uses explicit API versions:
### Version Numbers
@ -244,4 +244,4 @@ If you have questions or need help, please:
2. Check existing documentation
3. Reach out to the maintainers directly
Thank you for contributing to MCGhidra!
Thank you for contributing to GhydraMCP!

View File

@ -1,4 +1,4 @@
# MCGhidra Ghidra Plugin HTTP API v2
# GhydraMCP Ghidra Plugin HTTP API v2
## Overview
@ -159,7 +159,7 @@ Returns information about the current plugin instance, including details about t
```
### `GET /instances`
Returns information about all active MCGhidra plugin instances.
Returns information about all active GhydraMCP plugin instances.
```json
{
"id": "req-instances",

View File

@ -1,4 +1,4 @@
# MCGhidra Makefile
# GhydraMCP Makefile
# Convenient commands for Docker and development operations
.PHONY: help build build-dev up up-dev down down-dev logs logs-dev \
@ -6,7 +6,7 @@
# Default target
help:
@echo "MCGhidra Docker Management"
@echo "GhydraMCP Docker Management"
@echo "============================"
@echo ""
@echo "Build commands:"
@ -44,10 +44,10 @@ help:
# =============================================================================
build:
docker compose build mcghidra
docker compose build ghydramcp
build-dev:
docker compose build mcghidra-dev
docker compose build ghydramcp-dev
build-all: build build-dev
@ -56,14 +56,14 @@ build-all: build build-dev
# =============================================================================
up:
docker compose --profile prod up -d mcghidra
@echo "MCGhidra starting... checking health in 30 seconds"
docker compose --profile prod up -d ghydramcp
@echo "GhydraMCP starting... checking health in 30 seconds"
@sleep 30
@$(MAKE) health || echo "Server may still be starting up..."
up-dev:
docker compose --profile dev up -d mcghidra-dev
@echo "MCGhidra (dev) starting..."
docker compose --profile dev up -d ghydramcp-dev
@echo "GhydraMCP (dev) starting..."
down:
docker compose --profile prod down
@ -90,7 +90,7 @@ ifndef FILE
@exit 1
endif
@echo "Analyzing: $(FILE)"
docker compose run --rm -v "$(dir $(FILE)):/binaries:ro" mcghidra /binaries/$(notdir $(FILE))
docker compose run --rm -v "$(dir $(FILE)):/binaries:ro" ghydramcp /binaries/$(notdir $(FILE))
# Analyze in background (detached)
analyze-bg:
@ -99,20 +99,20 @@ ifndef FILE
@exit 1
endif
@echo "Starting background analysis of: $(FILE)"
docker compose run -d -v "$(dir $(FILE)):/binaries:ro" mcghidra /binaries/$(notdir $(FILE))
docker compose run -d -v "$(dir $(FILE)):/binaries:ro" ghydramcp /binaries/$(notdir $(FILE))
# =============================================================================
# Utility Commands
# =============================================================================
shell:
docker compose --profile debug run --rm mcghidra-shell
docker compose --profile debug run --rm ghydramcp-shell
logs:
docker compose logs -f mcghidra
docker compose logs -f ghydramcp
logs-dev:
docker compose logs -f mcghidra-dev
docker compose logs -f ghydramcp-dev
status:
@echo "=== Container Status ==="
@ -122,8 +122,8 @@ status:
@docker stats --no-stream $$(docker compose ps -q 2>/dev/null) 2>/dev/null || echo "No containers running"
health:
@echo "Checking MCGhidra API health..."
@curl -sf http://localhost:$${MCGHIDRA_PORT:-8192}/ | python3 -m json.tool 2>/dev/null \
@echo "Checking GhydraMCP API health..."
@curl -sf http://localhost:$${GHYDRA_PORT:-8192}/ | python3 -m json.tool 2>/dev/null \
|| echo "API not responding (server may be starting or binary being analyzed)"
# =============================================================================
@ -135,7 +135,7 @@ clean:
@echo "Containers and volumes removed"
clean-all: clean
docker rmi mcghidra:latest mcghidra:dev 2>/dev/null || true
docker rmi ghydramcp:latest ghydramcp:dev 2>/dev/null || true
@echo "Images removed"
prune:
@ -147,10 +147,10 @@ prune:
# =============================================================================
mcp:
uv run python -m mcghidra
uv run python -m ghydramcp
mcp-dev:
uv run python -m mcghidra --verbose
uv run python -m ghydramcp --verbose
# =============================================================================
# Development Commands

View File

@ -1,8 +1,8 @@
# MCGhidra Quick Start Guide
# GhydraMCP Quick Start Guide
## What is MCGhidra?
## What is GhydraMCP?
MCGhidra is a complete reverse engineering platform that combines:
GhydraMCP is a complete reverse engineering platform that combines:
- **Ghidra** - NSA's powerful binary analysis tool
- **Docker** - Containerized, reproducible analysis environment
- **HTTP REST API** - HATEOAS-compliant REST interface
@ -14,16 +14,16 @@ MCGhidra is a complete reverse engineering platform that combines:
### 1. Analyze a Standard Binary (ELF/PE/Mach-O)
```bash
cd /home/rpm/claude/mcghidra/MCGhidra
cd /home/rpm/claude/ghydramcp/GhydraMCP
# Build the Docker image (one time)
docker build -t mcghidra:latest -f docker/Dockerfile .
docker build -t ghydramcp:latest -f docker/Dockerfile .
# Analyze any standard binary
docker run -d --name my-analysis \
-p 8192:8192 \
-v $(pwd)/binaries:/binaries \
mcghidra:latest \
ghydramcp:latest \
/binaries/your-binary
# Wait ~20 seconds for analysis, then access HTTP API
@ -45,7 +45,7 @@ python3 docker/arm_firmware_prep.py \
docker run -d --name arm-firmware \
-p 8192:8192 \
-v $(pwd)/binaries:/binaries \
mcghidra:latest \
ghydramcp:latest \
/binaries/your-firmware.elf
```
@ -53,11 +53,11 @@ docker run -d --name arm-firmware \
```bash
# The MCP server is located at:
cd /home/rpm/claude/mcghidra/MCGhidra
cd /home/rpm/claude/ghydramcp/GhydraMCP
./launch.sh
# Or with uv:
cd MCGhidra && uv run mcghidra
cd GhydraMCP && uv run ghydramcp
```
## HTTP API Overview
@ -176,7 +176,7 @@ curl "http://localhost:8192/functions/$ENTRY/decompile" | jq -r '.result'
### List Running Containers
```bash
docker ps | grep mcghidra
docker ps | grep ghydramcp
```
### View Logs
@ -201,7 +201,7 @@ docker run -d --name persistent \
-v $(pwd)/projects:/projects \
-v $(pwd)/binaries:/binaries \
-e PROJECT_NAME=MyProject \
mcghidra:latest \
ghydramcp:latest \
/binaries/my-binary
# Projects are saved in ./projects/MyProject/
@ -238,7 +238,7 @@ docker exec my-analysis sh -c 'chmod 644 /opt/ghidra/scripts/*.java'
docker run -d --name analysis2 \
-p 8193:8192 \
-v $(pwd)/binaries:/binaries \
mcghidra:latest \
ghydramcp:latest \
/binaries/binary
# Access at http://localhost:8193/
@ -263,7 +263,7 @@ gcc -o binaries/test test.c
docker run -d --name test-analysis \
-p 8192:8192 \
-v $(pwd)/binaries:/binaries \
mcghidra:latest \
ghydramcp:latest \
/binaries/test
# Find hidden function
@ -284,7 +284,7 @@ python3 docker/arm_firmware_prep.py \
docker run -d --name cisco \
-p 8192:8192 \
-v $(pwd)/binaries:/binaries \
mcghidra:latest \
ghydramcp:latest \
/binaries/cisco.elf
# Explore
@ -303,15 +303,15 @@ curl http://localhost:8192/data/strings | jq '.strings[] | select(.value | test(
## Project Structure
```
MCGhidra/
GhydraMCP/
├── docker/
│ ├── Dockerfile # Main Docker image
│ ├── entrypoint.sh # Container entry point
│ ├── MCGhidraServer.java # HTTP API server (1724 lines)
│ ├── GhydraMCPServer.java # HTTP API server (1724 lines)
│ ├── ImportRawARM.java # Raw binary import script
│ ├── arm_firmware_prep.py # ELF wrapper tool ⭐
│ └── README*.md # Documentation
├── src/mcghidra/ # MCP server implementation
├── src/ghydramcp/ # MCP server implementation
│ ├── __init__.py
│ ├── server.py # FastMCP server
│ └── mixins/ # Modular functionality

753
README.md
View File

@ -1,302 +1,601 @@
# MCGhidra
**AI-native reverse engineering.** Give Claude (or any MCP client) direct access to Ghidra's analysis engine.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ "Analyze the authentication bypass in this firmware" │
│ │
│ Claude: I'll decompile the auth functions and trace the validation logic. │
│ │
│ [functions_list grep="auth|login|verify"] │
│ [functions_decompile name="verify_password"] │
│ [xrefs_list to_addr="0x0040156c"] │
│ [analysis_get_dataflow address="0x00401234" direction="backward"] │
│ │
│ Found it. The password check at 0x401580 compares against a hardcoded │
│ hash, but there's a debug backdoor at 0x401590 that bypasses validation │
│ when the username starts with "debug_". Let me show you the call graph... │
└─────────────────────────────────────────────────────────────────────────────┘
```
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/releases)
[![API Version](https://img.shields.io/badge/API-v2.1-orange)](https://github.com/starsong-consulting/GhydraMCP/blob/main/GHIDRA_HTTP_API.md)
[![GitHub stars](https://img.shields.io/github/stars/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/network/members)
[![GitHub contributors](https://img.shields.io/github/contributors/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/graphs/contributors)
[![Build Status](https://github.com/starsong-consulting/GhydraMCP/actions/workflows/build.yml/badge.svg)](https://github.com/starsong-consulting/GhydraMCP/actions/workflows/build.yml)
## What You Get
# GhydraMCP v2.1
**64 MCP tools** across 12 categories:
GhydraMCP is a powerful bridge between [Ghidra](https://ghidra-sre.org/) and AI assistants that enables comprehensive AI-assisted reverse engineering through the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/mcp).
| Category | Tools | What it does |
|----------|-------|--------------|
| **Functions** | 11 | Decompile, disassemble, rename, set signatures, list variables |
| **Data** | 8 | Create/modify data items, list strings, set types |
| **Structs** | 7 | Create structs, add/update fields, manage data types |
| **Symbols** | 9 | Create labels, rename symbols, list imports/exports |
| **Analysis** | 6 | Call graphs, data flow, cross-references, run analysis |
| **Memory** | 2 | Read/write raw bytes |
| **Variables** | 4 | List/rename function variables, set types |
| **Bookmarks** | 3 | Create/list/delete analysis bookmarks |
| **Enums/Typedefs** | 4 | Create enum and typedef data types |
| **Namespaces** | 2 | List namespaces and classes |
| **Segments** | 1 | List memory segments with permissions |
| **Docker** | 7 | Auto-start containers, health checks, session management |
![GhydraMCP logo](https://github.com/user-attachments/assets/86b9b2de-767c-4ed5-b082-510b8109f00f)
**13 analysis prompts** for common RE workflows:
- `malware_triage` — Quick capability assessment
- `identify_crypto` — Find crypto functions and constants
- `find_authentication` — Locate auth, license checks, credentials
- `analyze_protocol` — Reverse network/file protocols
- `trace_data_flow` — Taint analysis through functions
- And 8 more specialized prompts...
## Overview
**11 MCP resources** for quick enumeration without tool calls.
GhydraMCP v2.1 integrates three key components:
---
1. **Modular Ghidra Plugin**: Exposes Ghidra's powerful reverse engineering capabilities through a HATEOAS-driven REST API
2. **MCP Bridge**: A Python script that translates MCP requests into API calls with comprehensive type checking
3. **Multi-instance Architecture**: Connect multiple Ghidra instances to analyze different binaries simultaneously
## Quick Start
This architecture enables AI assistants like Claude to seamlessly:
- Decompile and analyze binary code with customizable output formats
- Map program structures, function relationships, and complex data types
- Perform advanced binary analysis (cross-references, call graphs, data flow, etc.)
- Make precise modifications to the analysis (rename, annotate, create/delete/modify data, etc.)
- Read memory directly and manipulate binary at a low level
- Navigate resources through discoverable HATEOAS links
### Option 1: Docker (Easiest)
GhydraMCP is based on [GhidraMCP by Laurie Wired](https://github.com/LaurieWired/GhidraMCP/) but has evolved into a comprehensive reverse engineering platform with enhanced multi-instance support, extensive data manipulation capabilities, and a robust HATEOAS-compliant API architecture.
No Ghidra installation needed. Analyze binaries in isolated containers.
# Features
```bash
# Build the image (once)
cd MCGhidra && docker build -t mcghidra:latest -f docker/Dockerfile .
GhydraMCP version 2.1 provides a comprehensive set of reverse engineering capabilities to AI assistants through its HATEOAS-driven API:
# Add to your MCP config
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
```
## Advanced Program Analysis
Then in Claude:
```
Analyze /path/to/suspicious.exe
```
- **Enhanced Decompilation**:
- Convert binary functions to readable C code
- Toggle between clean C-like pseudocode and raw decompiler output
- Show/hide syntax trees for detailed analysis
- Multiple simplification styles for different analysis approaches
Claude will auto-start a container, wait for analysis, and begin work.
- **Comprehensive Static Analysis**:
- Cross-reference analysis (find callers and callees)
- Complete call graph generation and traversal
- Data flow analysis with variable tracking
- Type propagation and reconstruction
- Function relationship mapping
### Option 2: Native Ghidra
- **Memory Operations**:
- Direct memory reading with hex and raw byte representation
- Address space navigation and mapping
- Memory segment analysis
1. **Install the Ghidra plugin:**
- Download latest [release](https://github.com/starsong-consulting/MCGhidra/releases)
- In Ghidra: `File → Install Extensions → +` → select the `.zip`
- Restart Ghidra
- Enable in `File → Configure → Developer → MCGhidraPlugin`
- **Symbol Management**:
- View and analyze imports and exports
- Identify library functions and dependencies
- Symbol table exploration and manipulation
- Namespace hierarchy visualization
2. **Add MCP server:**
```bash
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
```
## Interactive Reverse Engineering
3. **Open a binary in Ghidra**, then ask Claude to analyze it.
- **Code Understanding**:
- Explore function code with rich context
- Analyze data structures and complex types
- View disassembly with linking to decompiled code
- Examine function prototypes and signatures
---
- **Comprehensive Annotation**:
- Rename functions, variables, and data
- Add multiple comment types (EOL, plate, pre/post)
- Create and modify data types
- Set and update function signatures and prototypes
## How It Works
## Complete Data Manipulation
```
┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
│ Claude │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
│ (or other │ stdio │ (Python) │ REST API │ Plugin │
│ MCP client) │ │ │ │ (Java) │
└──────────────┘ └──────────────┘ └──────────────┘
```
- **Data Creation and Management**:
- Create new data items with specified types
- Delete existing data items
- Rename data items with proper scope handling
- Set and update data types for existing items
- Combined rename and retype operations
- Type definition management
- **Ghidra Plugin**: Exposes Ghidra's analysis via HTTP REST API (HATEOAS)
- **MCGhidra Server**: Translates MCP tool calls to API requests
- **Multi-instance**: Analyze multiple binaries simultaneously on different ports
- **Session isolation**: Docker containers get unique ports, preventing conflicts
- **Function Manipulation**:
- Rename functions with proper scoping
- Update function signatures with parameter information
- Modify local variable names and types
- Set function return types
---
## Multi-instance Support
## Usage Patterns
- Run multiple Ghidra instances simultaneously
- Analyze different binaries in parallel
- Connect to specific instances using port numbers
- Auto-discovery of running Ghidra instances
- Instance metadata with project and file information
- Plugin version and API checking for compatibility
### Set Current Instance (Then Forget About Ports)
## Program Navigation and Discovery
- List and search functions, classes, and namespaces
- View memory segments and layout
- Search by name, pattern, or signature
- Resource discovery through HATEOAS links
- Pagination for handling large result sets
- Filtering capabilities across all resources
# Installation
## Prerequisites
- Install [Ghidra](https://ghidra-sre.org)
- Python3
- MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
## Ghidra
First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/releases) from this repository. The "Complete" artifact contains the zipped Ghidra plugin and the Python MCP bridge. Unpack the outer archive, then, add the plugin to Ghidra:
1. Run Ghidra
2. Select `File` -> `Install Extensions`
3. Click the `+` button
4. Select the `GhydraMCP-[version].zip` file from the downloaded release
5. Restart Ghidra
6. Make sure the GhydraMCPPlugin is enabled in `File` -> `Configure` -> `Developer`
> **Note:** By default, the first CodeBrowser opened in Ghidra gets port 8192, the second gets 8193, and so on. You can check which ports are being used by looking at the Console in the Ghidra main (project) window - click the computer icon in the bottom right to "Open Console". Look for log entries like:
> ```
> (HydraMCPPlugin) Plugin loaded on port 8193
> (HydraMCPPlugin) HydraMCP HTTP server started on port 8193
> ```
>
> GhydraMCP now includes auto-discovery of running Ghidra instances, so manually registering each instance is typically not necessary. The MCP bridge will automatically discover and register instances on startup and periodically check for new ones.
Video Installation Guide:
https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3
## MCP Clients
GhydraMCP works with any MCP-compatible client using **stdio transport**. It has been tested and confirmed working with:
- **Claude Desktop** - Anthropic's official desktop application
- **Claude Code** - Anthropic's VS Code extension and CLI tool
- **Cline** - Popular VS Code extension for AI-assisted coding
See the [Client Setup](#client-setup) section below for detailed configuration instructions for each client.
## API Reference (Updated for v2.1)
### Available Tools
GhydraMCP v2.1 organizes tools into logical namespaces for better discoverability and organization:
**Instance Management** (`instances_*`):
- `instances_list`: List active Ghidra instances (auto-discovers on default host) - **use this first**
- `instances_discover`: Discover instances on a specific host (params: host [optional]) - **only use for non-default hosts**
- `instances_register`: Register new instance (params: port, url [optional])
- `instances_unregister`: Remove instance (params: port)
- `instances_use`: Set current working instance (params: port)
- `instances_current`: Get current working instance info
**Function Analysis** (`functions_*`):
- `functions_list`: List all functions (params: offset, limit, port [optional])
- `functions_get`: Get function details (params: name or address, port [optional])
- `functions_decompile`: Get decompiled C code (params: name or address, syntax_tree, style, timeout, port [optional])
- `functions_disassemble`: Get disassembled instructions (params: name or address, port [optional])
- `functions_create`: Create function at address (params: address, port [optional])
- `functions_rename`: Rename a function (params: old_name or address, new_name, port [optional])
- `functions_set_signature`: Update function prototype (params: name or address, signature, port [optional])
- `functions_get_variables`: Get function variables (params: name or address, port [optional])
- `functions_set_comment`: Set function comment (params: address, comment, port [optional])
**Data Manipulation** (`data_*`):
- `data_list`: List data items (params: offset, limit, addr, name, name_contains, port [optional])
- `data_list_strings`: List all defined strings (params: offset, limit, filter, port [optional])
- `data_create`: Create data at address (params: address, data_type, size [optional], port [optional])
- `data_rename`: Rename data item (params: address, name, port [optional])
- `data_delete`: Delete data item (params: address, port [optional])
- `data_set_type`: Change data type (params: address, data_type, port [optional])
**Struct Management** (`structs_*`):
- `structs_list`: List all struct data types (params: offset, limit, category [optional], port [optional])
- `structs_get`: Get detailed struct information (params: name, port [optional])
- `structs_create`: Create new struct (params: name, category [optional], description [optional], port [optional])
- `structs_add_field`: Add field to struct (params: struct_name, field_name, field_type, offset [optional], comment [optional], port [optional])
- `structs_update_field`: Update struct field (params: struct_name, field_name or field_offset, new_name [optional], new_type [optional], new_comment [optional], port [optional])
- `structs_delete`: Delete struct (params: name, port [optional])
**Memory Operations** (`memory_*`):
- `memory_read`: Read bytes from memory (params: address, length, format, port [optional])
- `memory_write`: Write bytes to memory (params: address, bytes_data, format, port [optional])
**Cross-References** (`xrefs_*`):
- `xrefs_list`: List cross-references (params: to_addr [optional], from_addr [optional], type [optional], offset, limit, port [optional])
**Analysis** (`analysis_*`):
- `analysis_run`: Trigger program analysis (params: port [optional], analysis_options [optional])
- `analysis_get_callgraph`: Get function call graph (params: name or address, max_depth, port [optional])
- `analysis_get_dataflow`: Perform data flow analysis (params: address, direction, max_steps, port [optional])
**Example Usage**:
```python
instances_list() # Discover running Ghidra instances
instances_use(port=8192) # Set as current
functions_list() # No port needed!
data_list_strings(grep="password") # Uses current instance
# Instance Management - Always start here
client.use_tool("ghydra", "instances_list") # Auto-discovers instances on localhost
client.use_tool("ghydra", "instances_use", {"port": 8192}) # Set working instance
client.use_tool("ghydra", "instances_current") # Check current instance
# Function Analysis
client.use_tool("ghydra", "functions_list", {"offset": 0, "limit": 100})
client.use_tool("ghydra", "functions_get", {"name": "main"})
client.use_tool("ghydra", "functions_decompile", {"address": "0x00401000"})
client.use_tool("ghydra", "functions_disassemble", {"name": "main"})
client.use_tool("ghydra", "functions_rename", {"address": "0x00401000", "new_name": "process_data"})
client.use_tool("ghydra", "functions_set_signature", {"address": "0x00401000", "signature": "int process_data(char* buf, int len)"})
client.use_tool("ghydra", "functions_set_comment", {"address": "0x00401000", "comment": "Main processing function"})
# Data Manipulation
client.use_tool("ghydra", "data_list_strings", {"filter": "password"}) # Find strings containing "password"
client.use_tool("ghydra", "data_list", {"offset": 0, "limit": 50})
client.use_tool("ghydra", "data_create", {"address": "0x00401234", "data_type": "int"})
client.use_tool("ghydra", "data_rename", {"address": "0x00401234", "name": "counter"})
client.use_tool("ghydra", "data_set_type", {"address": "0x00401238", "data_type": "char *"})
client.use_tool("ghydra", "data_delete", {"address": "0x0040123C"})
# Struct Management
client.use_tool("ghydra", "structs_create", {"name": "NetworkPacket", "category": "/network"})
client.use_tool("ghydra", "structs_add_field", {
"struct_name": "NetworkPacket",
"field_name": "header",
"field_type": "dword",
"comment": "Packet header"
})
client.use_tool("ghydra", "structs_add_field", {
"struct_name": "NetworkPacket",
"field_name": "data_ptr",
"field_type": "pointer"
})
client.use_tool("ghydra", "structs_update_field", {
"struct_name": "NetworkPacket",
"field_name": "header",
"new_name": "packet_header",
"new_comment": "Updated header field"
})
client.use_tool("ghydra", "structs_get", {"name": "NetworkPacket"})
client.use_tool("ghydra", "structs_list", {"category": "/network"})
# Memory Operations
client.use_tool("ghydra", "memory_read", {"address": "0x00401000", "length": 16, "format": "hex"})
client.use_tool("ghydra", "memory_write", {"address": "0x00401000", "bytes_data": "90909090", "format": "hex"})
# Cross-References
client.use_tool("ghydra", "xrefs_list", {"to_addr": "0x00401000"}) # Find callers
client.use_tool("ghydra", "xrefs_list", {"from_addr": "0x00401000"}) # Find callees
# Analysis
client.use_tool("ghydra", "analysis_get_callgraph", {"name": "main", "max_depth": 5})
client.use_tool("ghydra", "analysis_get_dataflow", {"address": "0x00401050", "direction": "forward"})
client.use_tool("ghydra", "analysis_run") # Trigger full analysis
```
### Docker Workflow
## Client Setup
```python
# Start container (returns immediately)
result = docker_auto_start(binary_path="/path/to/malware.exe")
# → {port: 8195, message: "Poll docker_health(port=8195)..."}
GhydraMCP works with any MCP-compatible client. Below are configuration examples for popular AI coding assistants.
# Poll until ready
while True:
health = docker_health(port=8195)
if health["healthy"]:
break
# Can check docker_logs() while waiting
### Installation Methods
# Register and use
instances_use(port=8195)
functions_list() # Ready to analyze
```
#### Recommended: Local Installation from Release
### Cursor-Based Pagination
Download the latest [release](https://github.com/starsong-consulting/GhydraMCP/releases) to ensure the bridge and plugin versions are in sync.
Large binaries can have 100K+ functions. Use cursors:
```python
result = functions_list(page_size=100)
# → {items: [...], cursor_id: "abc123", has_more: true}
# Get next page
cursor_next(cursor_id="abc123")
# Or filter server-side
functions_list(grep="crypto|encrypt", page_size=50)
```
### Analysis Prompts
Built-in prompts for common workflows:
```
/prompt malware_triage
/prompt identify_crypto
/prompt find_authentication
```
These guide Claude through systematic analysis with progress reporting.
---
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `GHIDRA_HYDRA_HOST` | `localhost` | Ghidra instance host |
| `GHIDRA_HYDRA_PORT` | `8192` | Default port |
### MCP Config Examples
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"mcghidra": {
"ghydra": {
"command": "uv",
"args": ["run", "--directory", "/path/to/MCGhidra", "mcghidra"]
"args": [
"run",
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
}
}
}
}
```
**Claude Code**:
```bash
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
Replace `/ABSOLUTE_PATH_TO/` with the actual path to your `bridge_mcp_hydra.py` file.
> **Note:** You can also use `python` instead of `uv run`, but then you'll need to manually install the requirements first with `pip install mcp requests`.
#### Alternative: Direct from Repository with uvx
If you want to use the latest development version, you can run directly from the GitHub repository:
```json
{
"mcpServers": {
"ghydra": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/starsong-consulting/GhydraMCP",
"ghydramcp"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
}
}
}
}
```
---
> **Warning:** This method may pull a bridge version that's out of sync with your installed plugin. Only use this if you're tracking the latest development branch.
## Tool Reference
### Claude Desktop Configuration
### Instance Management
```
instances_list # Discover Ghidra instances (use this first!)
instances_use # Set current working instance
instances_current # Show current instance info
Add your chosen configuration method to your Claude Desktop configuration file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
### Claude Code Configuration
Claude Code automatically discovers MCP servers configured in Claude Desktop. If you've set up the configuration above, Claude Code will have access to GhydraMCP tools immediately.
Alternatively, you can configure Claude Code separately by adding the same configuration to the MCP settings in Claude Code's configuration.
### Cline Configuration
Cline (VS Code extension) uses a separate configuration file. To set up GhydraMCP with Cline:
1. Open VS Code with Cline installed
2. Click the "MCP Servers" icon in Cline's interface
3. Select the "Configure" tab
4. Click "Configure MCP Servers" to edit `cline_mcp_settings.json`
5. Add the following configuration:
```json
{
"mcpServers": {
"ghydra": {
"command": "uv",
"args": [
"run",
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
},
"disabled": false
}
}
}
```
### Function Analysis
```
functions_list # List functions (supports grep, pagination)
functions_get # Get function details by name or address
functions_decompile # Decompile to C pseudocode
functions_disassemble # Get assembly instructions
functions_rename # Rename a function
functions_set_signature # Set function prototype
functions_set_comment # Add decompiler comment
functions_create # Create function at address
functions_variables # List local variables and parameters
If you prefer to use `python` directly instead of `uv`:
```json
{
"mcpServers": {
"ghydra": {
"command": "python",
"args": [
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
},
"disabled": false
}
}
}
```
### Data Operations
> **Important:** Replace `/ABSOLUTE_PATH_TO/` with the actual absolute path to your `bridge_mcp_hydra.py` file. For example:
> - **Linux/macOS**: `/home/username/GhydraMCP/bridge_mcp_hydra.py`
> - **Windows**: `C:\\Users\\username\\GhydraMCP\\bridge_mcp_hydra.py`
After saving the configuration, restart Cline to load the GhydraMCP server.
### 5ire Configuration
1. Tool Key: ghydra
2. Name: GhydraMCP
3. Command: `uv run /ABSOLUTE_PATH_TO/bridge_mcp_hydra.py`
## Example Session
Below is an example of how an AI assistant might interact with Ghidra using GhydraMCP v2.1:
**User:**
```
data_list # List defined data items
data_list_strings # List strings (with grep filtering)
data_create # Define data at address
data_rename # Rename data item
data_set_type # Change data type
data_delete # Remove data definition
Analyze the main function in the binary and identify any data structures being used.
```
### Cross-References & Analysis
**Assistant:**
```
xrefs_list # Find cross-references to/from address
analysis_get_callgraph # Generate call graph
analysis_get_dataflow # Trace data flow forward/backward
analysis_run # Trigger Ghidra auto-analysis
I'll analyze the binary for you. Let me start by checking available Ghidra instances.
[Uses instances_list]
Found Ghidra instance on port 8192 analyzing "malware_sample.exe".
[Uses instances_use with port 8192]
Set instance 8192 as current working instance.
[Uses functions_decompile with name "main"]
Here's the decompiled main function:
void main(void) {
NetworkPacket *packet;
int status;
packet = (NetworkPacket *)malloc(0x20);
packet->header = 0x1234;
packet->data_ptr = allocate_buffer(0x100);
status = process_packet(packet);
...
}
I can see this code uses a "NetworkPacket" structure. Let me check if it's already defined.
[Uses structs_get with name "NetworkPacket"]
The struct doesn't exist yet. Let me create it based on the decompiled code.
[Uses structs_create with name "NetworkPacket", category "/network"]
Created NetworkPacket struct.
[Uses structs_add_field for each field]
Added fields:
- header (dword at offset 0)
- data_ptr (pointer at offset 4)
- size (dword at offset 8)
- status (dword at offset 12)
[Uses data_list_strings with filter "Error"]
I also found several error strings that suggest this binary handles network communications with error checking.
The main function appears to be the entry point for a network packet processing routine that allocates and processes data structures for network communication.
```
### Structs & Types
**User:**
```
structs_list # List struct definitions
structs_get # Get struct with all fields
structs_create # Create new struct
structs_add_field # Add field to struct
structs_update_field # Modify existing field
structs_delete # Remove struct
enums_list / enums_create
typedefs_list / typedefs_create
Can you trace where process_packet is called from and show me the call graph?
```
### Docker Management
**Assistant:**
```
docker_auto_start # Start container for binary (auto port allocation)
docker_health # Check if container API is responding
docker_status # List all containers and images
docker_start # Manual container start
docker_stop # Stop container (session-scoped)
docker_logs # Get container logs
docker_cleanup # Remove orphaned containers
I'll trace the call relationships for process_packet.
[Uses xrefs_list with to_addr for process_packet]
Found 3 callers of process_packet:
- main at 0x401000
- worker_thread at 0x401200
- handle_request at 0x401450
[Uses analysis_get_callgraph with name "process_packet", max_depth 3]
Here's the call graph starting from process_packet:
process_packet (0x401100)
├── validate_header (0x401150)
│ └── check_magic (0x401180)
├── parse_data (0x4011A0)
│ ├── extract_field (0x4011D0)
│ └── validate_checksum (0x4011E0)
└── send_response (0x401220)
└── network_send (0x401250)
This shows process_packet coordinates validation, parsing, and response transmission.
```
See `--help` or the [API docs](GHIDRA_HTTP_API.md) for full parameter details.
# JSON Communication
---
GhydraMCP uses structured JSON for all communication between the Python bridge and Java plugin. This ensures consistent and reliable data exchange.
## Building from Source
## API Architecture
```bash
# Clone
git clone https://github.com/starsong-consulting/MCGhidra
cd MCGhidra
GhydraMCP v2.1 implements a comprehensive HATEOAS-driven REST API that follows hypermedia design principles:
# Build Ghidra plugin
### Core API Design
- **HATEOAS Architecture**: Each response includes navigational links for resource discovery
- **Versioned Endpoints**: All requests verified against API version for compatibility
- **Structured Responses**: Standardized JSON format with consistent field naming
- **Proper HTTP Methods**: GET for retrieval, POST for creation, PATCH for updates, DELETE for removal
- **Appropriate Status Codes**: Uses standard HTTP status codes for clear error handling
### Response Format
All responses follow this HATEOAS-driven format:
```json
{
"id": "req-123",
"instance": "http://localhost:8192",
"success": true,
"result": "...",
"timestamp": 1712159482123,
"_links": {
"self": {"href": "/endpoint/current"},
"related": [
{"href": "/endpoint/related1", "name": "Related Resource 1"},
{"href": "/endpoint/related2", "name": "Related Resource 2"}
]
}
}
```
For list responses, pagination information is included:
```json
{
"id": "req-123",
"instance": "http://localhost:8192",
"success": true,
"result": [ ... objects ... ],
"size": 150,
"offset": 0,
"limit": 50,
"_links": {
"self": { "href": "/functions?offset=0&limit=50" },
"next": { "href": "/functions?offset=50&limit=50" },
"prev": { "href": "/functions?offset=0&limit=50" }
}
}
```
Error responses include detailed information:
```json
{
"id": "req-123",
"instance": "http://localhost:8192",
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Function 'main' not found in current program"
},
"status_code": 404,
"timestamp": 1712159482123,
"_links": {
"self": {"href": "/functions/main"}
}
}
```
This HATEOAS approach enables resource discovery and self-documenting APIs, making integration and exploration significantly easier.
# Testing
GhydraMCP includes comprehensive test suites for both the HTTP API and MCP bridge. See [TESTING.md](TESTING.md) for details on running the tests.
## HTTP API Tests
Tests the HTTP endpoints exposed by the Java plugin:
- Response format and structure
- JSON structure consistency
- Required fields in responses
- Error handling
## MCP Bridge Tests
Tests the MCP bridge functionality:
- MCP protocol communication
- Tool availability and structure
- Response format and structure
- JSON structure consistency
# Building from Source
You can build different artifacts with Maven:
## Build Everything (Default)
Build both the Ghidra plugin and the complete package:
```
mvn clean package
# → target/MCGhidra-[version].zip
# Build Docker image
docker build -t mcghidra:latest -f docker/Dockerfile .
# Run MCP server (for development)
uv run mcghidra
```
---
This creates:
- `target/GhydraMCP-[version].zip` - The Ghidra plugin only
- `target/GhydraMCP-Complete-[version].zip` - Complete package with plugin and bridge script
## Architecture
## Build Ghidra Plugin Only
If you only need the Ghidra plugin:
MCGhidra is designed for AI agents:
```
mvn clean package -P plugin-only
```
- **Lazy registration**: `instances_use` doesn't block — validates on first real call
- **Non-blocking I/O**: All Docker/HTTP operations run in thread executors
- **Session isolation**: Each MCP session gets unique container ports
- **Cursor pagination**: Handle 100K+ item responses without context overflow
- **Server-side grep**: Filter results before they hit the wire
## Build Complete Package Only
If you only need the combined package:
Based on [GhidraMCP by Laurie Wired](https://github.com/LaurieWired/GhidraMCP/), evolved into a comprehensive RE platform.
```
mvn clean package -P complete-only
```
---
## License
Apache 2.0
The Ghidra plugin includes these files required for Ghidra to recognize the extension:
- lib/GhydraMCP.jar
- extension.properties
- Module.manifest

View File

@ -1,11 +1,11 @@
# Testing MCGhidra
# Testing GhydraMCP
This document describes how to test the MCGhidra plugin and bridge.
This document describes how to test the GhydraMCP plugin and bridge.
## Prerequisites
- Python 3.11 or higher
- Ghidra with the MCGhidra plugin installed and running
- Ghidra with the GhydraMCP plugin installed and running
- The `requests` Python package (`pip install requests`)
## Running All Tests
@ -34,7 +34,7 @@ The `test_http_api.py` script tests the HTTP API exposed by the Java plugin. It
### Running the HTTP API Tests
1. Make sure Ghidra is running with the MCGhidra plugin loaded
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
2. Run the tests:
```bash
@ -57,7 +57,7 @@ The `test_mcp_client.py` script tests the MCP bridge functionality using the MCP
### Running the MCP Bridge Tests
1. Make sure Ghidra is running with the MCGhidra plugin loaded
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
2. Run the tests:
```bash
@ -89,7 +89,7 @@ The test script will:
### HTTP API Tests
- If tests are skipped with "Ghidra server not running or not accessible", make sure Ghidra is running and the MCGhidra plugin is loaded.
- If tests are skipped with "Ghidra server not running or not accessible", make sure Ghidra is running and the GhydraMCP plugin is loaded.
- If tests fail with connection errors, check that the plugin is listening on the expected port (default: 8192).
### MCP Bridge Tests
@ -103,7 +103,7 @@ The test script will:
To add a new test for an HTTP endpoint:
1. Add a new test method to the `MCGhidraHttpApiTests` class
1. Add a new test method to the `GhydraMCPHttpApiTests` class
2. Use the `requests` library to make HTTP requests to the endpoint
3. Verify the response using assertions

View File

@ -5,7 +5,7 @@
# "requests>=2.32.3",
# ]
# ///
# MCGhidra Bridge for Ghidra HATEOAS API - Optimized for MCP integration
# GhydraMCP Bridge for Ghidra HATEOAS API - Optimized for MCP integration
# Provides namespaced tools for interacting with Ghidra's reverse engineering capabilities
# Features: Cursor-based pagination, grep filtering, session isolation
import os
@ -699,7 +699,7 @@ def paginate_response(data: List[Any], query_params: dict,
# ================= End Cursor System =================
instructions = """
MCGhidra allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
GhydraMCP allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
First, run `instances_list()` to see all available Ghidra instances (automatically discovers instances on the default host).
Then use `instances_use(port)` to set your working instance.
@ -742,7 +742,7 @@ Use `cursor_list()` to see active cursors.
Use `cursor_delete(cursor_id)` to clean up cursors.
"""
mcp = FastMCP("MCGhidra", instructions=instructions)
mcp = FastMCP("GhydraMCP", instructions=instructions)
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
@ -1162,7 +1162,7 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
timeout=timeout)
if response.ok:
# Further validate it's a MCGhidra instance by checking response format
# Further validate it's a GhydraMCP instance by checking response format
try:
json_data = response.json()
if "success" in json_data and json_data["success"] and "result" in json_data:
@ -2200,7 +2200,7 @@ def reverse_engineer_binary_prompt(port: int = None):
- Security checks
- Data transformation
Remember to use the available MCGhidra tools:
Remember to use the available GhydraMCP tools:
- Use functions_list to find functions matching patterns
- Use xrefs_list to find cross-references
- Use functions_decompile for C-like representations
@ -6862,7 +6862,7 @@ def main():
discovery_thread = threading.Thread(
target=periodic_discovery,
daemon=True,
name="MCGhidra-Discovery"
name="GhydraMCP-Discovery"
)
discovery_thread.start()

View File

@ -1,9 +1,9 @@
# MCGhidra Docker Compose Configuration
# Provides both development and production modes for Ghidra + MCGhidra
# GhydraMCP Docker Compose Configuration
# Provides both development and production modes for Ghidra + GhydraMCP
#
# Usage:
# Development: docker compose up mcghidra-dev
# Production: docker compose up mcghidra
# Development: docker compose up ghydramcp-dev
# Production: docker compose up ghydramcp
#
# Set MODE in .env file to switch between dev/prod behaviors
@ -11,28 +11,28 @@ services:
# =============================================================================
# Production Service - Optimized for stability and security
# =============================================================================
mcghidra:
ghydramcp:
build:
context: .
dockerfile: docker/Dockerfile
args:
GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
image: mcghidra:${MCGHIDRAMCP_VERSION:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-server
image: ghydramcp:${GHYDRAMCP_VERSION:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-server
restart: unless-stopped
ports:
- "${MCGHIDRA_PORT:-8192}:8192"
- "${GHYDRA_PORT:-8192}:8192"
volumes:
# Mount binaries to analyze (read-only in prod)
- ${BINARIES_PATH:-./binaries}:/binaries:ro
# Persist Ghidra projects between runs
- mcghidra-projects:/projects
- ghydra-projects:/projects
environment:
- MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
- MCGHIDRA_PORT=8192
- MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-2G}
- PROJECT_NAME=${PROJECT_NAME:-MCGhidra}
- GHYDRA_MODE=${GHYDRA_MODE:-headless}
- GHYDRA_PORT=8192
- GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-2G}
- PROJECT_NAME=${PROJECT_NAME:-GhydraMCP}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8192/"]
interval: 30s
@ -42,7 +42,7 @@ services:
deploy:
resources:
limits:
memory: ${MCGHIDRA_MAXMEM:-2G}
memory: ${GHYDRA_MAXMEM:-2G}
profiles:
- prod
- default
@ -50,17 +50,17 @@ services:
# =============================================================================
# Development Service - Hot-reload and debugging friendly
# =============================================================================
mcghidra-dev:
ghydramcp-dev:
build:
context: .
dockerfile: docker/Dockerfile
args:
GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
image: mcghidra:dev
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-dev
image: ghydramcp:dev
container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-dev
ports:
- "${MCGHIDRA_PORT:-8192}:8192"
- "${GHYDRA_PORT:-8192}:8192"
# Additional ports for debugging/multiple instances
- "8193:8193"
- "8194:8194"
@ -68,15 +68,15 @@ services:
# Mount binaries (read-write in dev)
- ${BINARIES_PATH:-./binaries}:/binaries:rw
# Persist projects
- mcghidra-projects-dev:/projects
- ghydra-projects-dev:/projects
# Mount scripts for live editing (development only)
- ./docker/MCGhidraServer.java:/opt/ghidra/scripts/MCGhidraServer.java:ro
- ./docker/GhydraMCPServer.java:/opt/ghidra/scripts/GhydraMCPServer.java:ro
- ./docker/entrypoint.sh:/entrypoint.sh:ro
environment:
- MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
- MCGHIDRA_PORT=8192
- MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-4G}
- PROJECT_NAME=${PROJECT_NAME:-MCGhidra-Dev}
- GHYDRA_MODE=${GHYDRA_MODE:-headless}
- GHYDRA_PORT=8192
- GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-4G}
- PROJECT_NAME=${PROJECT_NAME:-GhydraMCP-Dev}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8192/"]
interval: 15s
@ -89,28 +89,28 @@ services:
# =============================================================================
# Shell Service - Interactive debugging container
# =============================================================================
mcghidra-shell:
ghydramcp-shell:
build:
context: .
dockerfile: docker/Dockerfile
image: mcghidra:${MCGHIDRAMCP_VERSION:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-shell
image: ghydramcp:${GHYDRAMCP_VERSION:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-shell
stdin_open: true
tty: true
volumes:
- ${BINARIES_PATH:-./binaries}:/binaries:rw
- mcghidra-projects-dev:/projects
- ghydra-projects-dev:/projects
environment:
- MCGHIDRA_MODE=shell
- GHYDRA_MODE=shell
profiles:
- debug
volumes:
mcghidra-projects:
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-projects
mcghidra-projects-dev:
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-projects-dev
ghydra-projects:
name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-projects
ghydra-projects-dev:
name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-projects-dev
networks:
default:
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-network
name: ${COMPOSE_PROJECT_NAME:-ghydramcp}-network

View File

@ -1,14 +1,14 @@
# MCGhidra Docker Image
# Ghidra + MCGhidra Plugin pre-installed for headless binary analysis
# GhydraMCP Docker Image
# Ghidra + GhydraMCP Plugin pre-installed for headless binary analysis
#
# Build: docker build -t mcghidra:latest -f docker/Dockerfile .
# Run: docker run -p 8192:8192 -v /path/to/binaries:/binaries mcghidra:latest
# Build: docker build -t ghydramcp:latest -f docker/Dockerfile .
# Run: docker run -p 8192:8192 -v /path/to/binaries:/binaries ghydramcp:latest
ARG GHIDRA_VERSION=11.4.2
ARG GHIDRA_DATE=20250826
# =============================================================================
# Stage 1: Build the MCGhidra plugin
# Stage 1: Build the GhydraMCP plugin
# =============================================================================
FROM eclipse-temurin:21-jdk-jammy AS builder
@ -25,19 +25,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Download and extract Ghidra
WORKDIR /opt
# Download with retries and resume support for unreliable connections
RUN for i in 1 2 3 4 5; do \
curl -fSL --http1.1 -C - \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip && break || sleep 30; \
done \
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip \
&& unzip -q ghidra.zip \
&& rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra
ENV GHIDRA_HOME=/opt/ghidra
# Copy MCGhidra source and build
# Copy GhydraMCP source and build
WORKDIR /build
# Copy pom.xml first and download dependencies (cached until pom.xml changes)
@ -67,7 +63,7 @@ RUN mvn package -P plugin-only -DskipTests \
-Dghidra.base.jar=${GHIDRA_HOME}/Ghidra/Features/Base/lib/Base.jar
# =============================================================================
# Stage 2: Runtime image with Ghidra + MCGhidra
# Stage 2: Runtime image with Ghidra + GhydraMCP
# =============================================================================
# NOTE: Ghidra requires JDK (not JRE) - it checks for javac in LaunchSupport
FROM eclipse-temurin:21-jdk-jammy AS runtime
@ -75,9 +71,9 @@ FROM eclipse-temurin:21-jdk-jammy AS runtime
ARG GHIDRA_VERSION
ARG GHIDRA_DATE
LABEL org.opencontainers.image.title="mcghidra" \
org.opencontainers.image.description="Ghidra + MCGhidra Plugin for AI-assisted reverse engineering" \
org.opencontainers.image.source="https://github.com/starsong-consulting/MCGhidra" \
LABEL org.opencontainers.image.title="ghydramcp" \
org.opencontainers.image.description="Ghidra + GhydraMCP Plugin for AI-assisted reverse engineering" \
org.opencontainers.image.source="https://github.com/starsong-consulting/GhydraMCP" \
org.opencontainers.image.licenses="Apache-2.0"
# Install runtime dependencies
@ -93,12 +89,8 @@ RUN groupadd -g 1001 ghidra && useradd -u 1001 -g ghidra -m -s /bin/bash ghidra
# Download and extract Ghidra (in runtime stage for cleaner image)
WORKDIR /opt
# Download with retries and resume support for unreliable connections
RUN for i in 1 2 3 4 5; do \
curl -fSL --http1.1 -C - \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip && break || sleep 30; \
done \
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip \
&& unzip -q ghidra.zip \
&& rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \
@ -107,21 +99,21 @@ RUN for i in 1 2 3 4 5; do \
ENV GHIDRA_HOME=/opt/ghidra
ENV PATH="${GHIDRA_HOME}:${PATH}"
# Install the MCGhidra plugin
COPY --from=builder /build/target/MCGhidra-*.zip /tmp/
# Install the GhydraMCP plugin
COPY --from=builder /build/target/GhydraMCP-*.zip /tmp/
RUN mkdir -p /opt/ghidra/Ghidra/Extensions \
&& unzip -q /tmp/MCGhidra-*.zip -d /opt/ghidra/Ghidra/Extensions/ \
&& rm /tmp/MCGhidra-*.zip \
&& unzip -q /tmp/GhydraMCP-*.zip -d /opt/ghidra/Ghidra/Extensions/ \
&& rm /tmp/GhydraMCP-*.zip \
&& chown -R ghidra:ghidra /opt/ghidra/Ghidra/Extensions/
# Create directories for projects and binaries
RUN mkdir -p /projects /binaries /home/ghidra/.ghidra \
&& chown -R ghidra:ghidra /projects /binaries /home/ghidra
# Copy MCGhidra Python scripts to user scripts directory
# Copy GhydraMCP Python scripts to user scripts directory
# Python/Jython scripts don't require OSGi bundle registration - they work without issue
RUN mkdir -p /home/ghidra/ghidra_scripts
COPY docker/MCGhidraServer.py /home/ghidra/ghidra_scripts/
COPY docker/GhydraMCPServer.py /home/ghidra/ghidra_scripts/
COPY docker/ImportRawARM.java /home/ghidra/ghidra_scripts/
# Set proper ownership and permissions
@ -137,16 +129,16 @@ RUN chmod 755 /entrypoint.sh
USER ghidra
WORKDIR /home/ghidra
# Expose the MCGhidra HTTP API port (and additional ports for multiple instances)
# Expose the GhydraMCP HTTP API port (and additional ports for multiple instances)
EXPOSE 8192 8193 8194 8195
# Default environment
ENV MCGHIDRA_MODE=headless
ENV MCGHIDRA_PORT=8192
ENV MCGHIDRA_MAXMEM=2G
ENV GHYDRA_MODE=headless
ENV GHYDRA_PORT=8192
ENV GHYDRA_MAXMEM=2G
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${MCGHIDRA_PORT}/health || exit 1
CMD curl -f http://localhost:${GHYDRA_PORT}/ || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,10 +1,10 @@
# MCGhidraServer.py - Headless Ghidra script for MCGhidra HTTP API
# GhydraMCPServer.py - Headless Ghidra script for GhydraMCP HTTP API
# Full API parity with the Java plugin implementation.
# Python 2 / Jython compatible (no f-strings, no readAllBytes).
#
# Usage: analyzeHeadless <project> <name> -import <binary> -postScript MCGhidraServer.py [port]
# Usage: analyzeHeadless <project> <name> -import <binary> -postScript GhydraMCPServer.py [port]
#
#@category MCGhidra
#@category GhydraMCP
#@keybinding
#@menupath
#@toolbar
@ -366,7 +366,7 @@ ROUTES = [
# HTTP Handler
# ========================================================================
class MCGhidraHandler(HttpHandler):
class GhydraMCPHandler(HttpHandler):
def __init__(self, program, decompiler):
self.program = program
@ -412,28 +412,12 @@ class MCGhidraHandler(HttpHandler):
"success": False,
"error": {"code": "NOT_FOUND", "message": "Endpoint not found: %s %s" % (method, path)}
})
except:
# Catch ALL exceptions including Java exceptions
import sys
exc_info = sys.exc_info()
try:
# Try to get a string representation safely
if exc_info[1] is not None:
msg = str(exc_info[1])
else:
msg = str(exc_info[0])
except:
msg = "Unknown exception"
except Exception as e:
try:
self._send_response(exchange, 500, {
"success": False,
"error": {"code": "INTERNAL_ERROR", "message": msg}
"error": {"code": "INTERNAL_ERROR", "message": str(e)}
})
except:
# Last resort - at least don't crash silently
try:
exchange.sendResponseHeaders(500, 0)
exchange.getResponseBody().close()
except:
pass
@ -657,7 +641,7 @@ class MCGhidraHandler(HttpHandler):
"success": True,
"api_version": API_VERSION,
"api_version_string": API_VERSION_STRING,
"message": "MCGhidra Headless API",
"message": "GhydraMCP Headless API",
"mode": "headless",
}
if self.program:
@ -1008,9 +992,6 @@ class MCGhidraHandler(HttpHandler):
except:
return {"success": False, "error": {"code": "INVALID_ADDRESS", "message": "Invalid address: %s" % addr_str}}
if addr is None:
return {"success": False, "error": {"code": "INVALID_ADDRESS", "message": "Could not parse address: %s" % addr_str}}
result_holder = [None]
def do_create():
@ -1028,14 +1009,8 @@ class MCGhidraHandler(HttpHandler):
"message": "Function created successfully",
}}, 201)
return {"success": False, "error": {"code": "CREATE_FAILED", "message": "Failed to create function at %s" % addr_str}}
except:
import sys
exc = sys.exc_info()[1]
try:
msg = str(exc)
except:
msg = "Failed to create function"
return {"success": False, "error": {"code": "CREATE_ERROR", "message": msg}}
except Exception as e:
return {"success": False, "error": {"code": "CREATE_ERROR", "message": str(e)}}
# -- Signature --
@ -1181,9 +1156,6 @@ class MCGhidraHandler(HttpHandler):
except:
return {"success": False, "error": {"code": "INVALID_ADDRESS", "message": "Invalid address: %s" % addr_str}}
if addr is None:
return {"success": False, "error": {"code": "INVALID_ADDRESS", "message": "Could not parse address: %s" % addr_str}}
# Label creation (newName field)
new_name = body.get("newName")
if new_name:
@ -1192,14 +1164,8 @@ class MCGhidraHandler(HttpHandler):
try:
with_transaction(self.program, "Create label", do_label)
return {"success": True, "result": {"address": addr_str, "name": new_name, "message": "Label created"}}
except:
import sys
exc = sys.exc_info()[1]
try:
msg = str(exc)
except:
msg = "Failed to create label"
return {"success": False, "error": {"code": "LABEL_ERROR", "message": msg}}
except Exception as e:
return {"success": False, "error": {"code": "LABEL_ERROR", "message": str(e)}}
# Data creation (type field)
type_name = body.get("type")
@ -1221,14 +1187,8 @@ class MCGhidraHandler(HttpHandler):
try:
with_transaction(self.program, "Create data", do_create_data)
return ({"success": True, "result": {"address": addr_str, "type": type_name, "message": "Data created"}}, 201)
except:
import sys
exc = sys.exc_info()[1]
try:
msg = str(exc)
except:
msg = "Failed to create data"
return {"success": False, "error": {"code": "DATA_ERROR", "message": msg}}
except Exception as e:
return {"success": False, "error": {"code": "DATA_ERROR", "message": str(e)}}
def handle_data_delete(self, exchange):
if not self.program:
@ -2788,10 +2748,10 @@ class MCGhidraHandler(HttpHandler):
def run_server(port, program, decompiler):
"""Start the HTTP server with a single catch-all handler."""
server = HttpServer.create(InetSocketAddress(port), 0)
server.createContext("/", MCGhidraHandler(program, decompiler))
server.createContext("/", GhydraMCPHandler(program, decompiler))
server.setExecutor(Executors.newCachedThreadPool())
server.start()
println("[MCGhidra] HTTP server started on port %d" % port)
println("[GhydraMCP] HTTP server started on port %d" % port)
return server
@ -2815,7 +2775,7 @@ def main():
decompiler.openProgram(currentProgram)
println("=========================================")
println(" MCGhidra Headless HTTP Server")
println(" GhydraMCP Headless HTTP Server")
println("=========================================")
println(" API Version: %s (compat: %d)" % (API_VERSION_STRING, API_VERSION))
println(" Port: %d" % port)
@ -2827,7 +2787,7 @@ def main():
server = run_server(port, currentProgram, decompiler)
println("")
println("MCGhidra Server running. Press Ctrl+C to stop.")
println("GhydraMCP Server running. Press Ctrl+C to stop.")
println("API available at: http://localhost:%d/" % port)
# Keep the script running
@ -2836,7 +2796,7 @@ def main():
time.sleep(1)
except KeyboardInterrupt:
server.stop(0)
println("[MCGhidra] Server stopped.")
println("[GhydraMCP] Server stopped.")
# Run

View File

@ -1,6 +1,6 @@
// Import and analyze raw ARM firmware binary
// This script imports a raw binary file with specified ARM processor and load address
// @author MCGhidra
// @author GhydraMCP
// @category Binary.Import
// @keybinding
// @menupath

View File

@ -1,15 +1,15 @@
# MCGhidra Docker Setup
# GhydraMCP Docker Setup
This directory contains Docker configuration for running MCGhidra in headless mode.
This directory contains Docker configuration for running GhydraMCP in headless mode.
## Quick Start
```bash
# Build the image
docker build -t mcghidra:latest -f docker/Dockerfile .
docker build -t ghydramcp:latest -f docker/Dockerfile .
# Analyze a binary
docker run -p 8192:8192 -v /path/to/binaries:/binaries mcghidra /binaries/sample.exe
docker run -p 8192:8192 -v /path/to/binaries:/binaries ghydramcp /binaries/sample.exe
# Check API health
curl http://localhost:8192/
@ -20,17 +20,17 @@ curl http://localhost:8192/
The Docker container includes:
1. **Ghidra 11.4.2** - Full headless installation
2. **MCGhidra Extension** - The Java plugin (installed in Extensions/)
3. **MCGhidraServer.py** - Headless HTTP server (Jython, full API parity)
2. **GhydraMCP Extension** - The Java plugin (installed in Extensions/)
3. **GhydraMCPServer.py** - Headless HTTP server (Jython, full API parity)
### Why Two HTTP Servers?
The MCGhidra plugin (`MCGhidraPlugin.java`) is a full Ghidra GUI plugin that requires:
The GhydraMCP plugin (`GhydraMCPPlugin.java`) is a full Ghidra GUI plugin that requires:
- Ghidra's `PluginTool` framework
- `ProgramManager` service for program access
- GUI event handling
These GUI services don't exist in headless mode. Instead, the container uses `MCGhidraServer.py`, a Jython script that:
These GUI services don't exist in headless mode. Instead, the container uses `GhydraMCPServer.py`, a Jython script that:
- Runs via `analyzeHeadless -postScript`
- Has direct access to `currentProgram` from the script context
- Provides **full API parity** with the GUI plugin (45 routes)
@ -38,7 +38,7 @@ These GUI services don't exist in headless mode. Instead, the container uses `MC
### Available Endpoints (Headless Mode)
The headless server implements the complete MCGhidra HTTP API:
The headless server implements the complete GhydraMCP HTTP API:
| Category | Endpoints | Description |
|----------|-----------|-------------|
@ -65,7 +65,7 @@ Imports a binary, analyzes it, and starts the HTTP API server:
```bash
docker run -p 8192:8192 \
-v ./samples:/binaries \
mcghidra /binaries/sample.exe
ghydramcp /binaries/sample.exe
```
### Server Mode
@ -74,9 +74,9 @@ Opens an existing project and program:
```bash
docker run -p 8192:8192 \
-e MCGHIDRA_MODE=server \
-e GHYDRA_MODE=server \
-v ./projects:/projects \
mcghidra program_name
ghydramcp program_name
```
### Analyze Mode
@ -85,10 +85,10 @@ Imports and analyzes without starting HTTP server:
```bash
docker run \
-e MCGHIDRA_MODE=analyze \
-e GHYDRA_MODE=analyze \
-v ./samples:/binaries \
-v ./projects:/projects \
mcghidra /binaries/sample.exe
ghydramcp /binaries/sample.exe
```
### Shell Mode
@ -97,19 +97,19 @@ Interactive debugging:
```bash
docker run -it \
-e MCGHIDRA_MODE=shell \
mcghidra
-e GHYDRA_MODE=shell \
ghydramcp
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRA_MODE` | `headless` | Container mode (headless, server, analyze, shell) |
| `MCGHIDRA_PORT` | `8192` | HTTP API port |
| `MCGHIDRA_MAXMEM` | `2G` | JVM heap memory |
| `GHYDRA_MODE` | `headless` | Container mode (headless, server, analyze, shell) |
| `GHYDRA_PORT` | `8192` | HTTP API port |
| `GHYDRA_MAXMEM` | `2G` | JVM heap memory |
| `PROJECT_DIR` | `/projects` | Ghidra project directory |
| `PROJECT_NAME` | `MCGhidra` | Ghidra project name |
| `PROJECT_NAME` | `GhydraMCP` | Ghidra project name |
## Docker Compose
@ -117,18 +117,18 @@ Use docker-compose for easier management:
```bash
# Development mode (hot-reload scripts)
docker compose --profile dev up mcghidra-dev
docker compose --profile dev up ghydramcp-dev
# Production mode
docker compose --profile prod up mcghidra
docker compose --profile prod up ghydramcp
# Interactive shell
docker compose --profile debug run --rm mcghidra-shell
docker compose --profile debug run --rm ghydramcp-shell
```
## MCP Integration
The MCGhidra Python server includes Docker management tools:
The GhydraMCP Python server includes Docker management tools:
```python
# Check Docker status
@ -144,10 +144,10 @@ await docker_wait(port=8192, timeout=300)
await docker_auto_start(binary_path="/path/to/binary.exe")
# Get container logs
await docker_logs("mcghidra-server")
await docker_logs("ghydramcp-server")
# Stop container
await docker_stop("mcghidra-server")
await docker_stop("ghydramcp-server")
```
## Building
@ -157,10 +157,10 @@ await docker_stop("mcghidra-server")
make build
# Using Docker directly
docker build -t mcghidra:latest -f docker/Dockerfile .
docker build -t ghydramcp:latest -f docker/Dockerfile .
# Build with specific Ghidra version
docker build -t mcghidra:latest \
docker build -t ghydramcp:latest \
--build-arg GHIDRA_VERSION=11.4.2 \
--build-arg GHIDRA_DATE=20250826 \
-f docker/Dockerfile .
@ -172,21 +172,21 @@ docker build -t mcghidra:latest \
Analysis takes time. Monitor progress with:
```bash
docker logs -f mcghidra-server
docker logs -f ghydramcp-server
```
### Port already in use
Stop existing containers:
```bash
docker stop $(docker ps -q --filter "name=mcghidra")
docker stop $(docker ps -q --filter "name=ghydramcp")
```
### Memory issues with large binaries
Increase JVM heap:
```bash
docker run -e MCGHIDRA_MAXMEM=4G -p 8192:8192 mcghidra /binaries/large.exe
docker run -e GHYDRA_MAXMEM=4G -p 8192:8192 ghydramcp /binaries/large.exe
```
### Permission denied on volumes

View File

@ -1,26 +1,26 @@
#!/bin/bash
# MCGhidra Docker Entrypoint
# GhydraMCP Docker Entrypoint
# Starts Ghidra in headless mode with HTTP API server
set -e
MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
MCGHIDRA_PORT=${MCGHIDRA_PORT:-8192}
MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-2G}
GHYDRA_MODE=${GHYDRA_MODE:-headless}
GHYDRA_PORT=${GHYDRA_PORT:-8192}
GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-2G}
GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra}
# User scripts directory - Python scripts don't need OSGi bundle registration
SCRIPT_DIR=${SCRIPT_DIR:-/home/ghidra/ghidra_scripts}
# Project settings
PROJECT_DIR=${PROJECT_DIR:-/projects}
PROJECT_NAME=${PROJECT_NAME:-MCGhidra}
PROJECT_NAME=${PROJECT_NAME:-GhydraMCP}
echo "=============================================="
echo " MCGhidra Docker Container"
echo " GhydraMCP Docker Container"
echo "=============================================="
echo " Mode: ${MCGHIDRA_MODE}"
echo " Port: ${MCGHIDRA_PORT}"
echo " Memory: ${MCGHIDRA_MAXMEM}"
echo " Mode: ${GHYDRA_MODE}"
echo " Port: ${GHYDRA_PORT}"
echo " Memory: ${GHYDRA_MAXMEM}"
echo " Project: ${PROJECT_DIR}/${PROJECT_NAME}"
echo "=============================================="
@ -28,29 +28,26 @@ echo "=============================================="
mkdir -p "${PROJECT_DIR}"
# Handle different modes
case "${MCGHIDRA_MODE}" in
case "${GHYDRA_MODE}" in
headless)
# Headless mode: Import a binary and start HTTP server
if [ $# -eq 0 ]; then
echo ""
echo "Usage: docker run mcghidra:latest [binary_path] [options]"
echo "Usage: docker run ghydramcp:latest [binary_path] [options]"
echo ""
echo "Examples:"
echo " # Analyze a binary mounted at /binaries/sample.exe"
echo " docker run -p 8192:8192 -v ./samples:/binaries mcghidra /binaries/sample.exe"
echo " docker run -p 8192:8192 -v ./samples:/binaries ghydramcp /binaries/sample.exe"
echo ""
echo " # With custom project name"
echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware mcghidra /binaries/sample.exe"
echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware ghydramcp /binaries/sample.exe"
echo ""
echo "Environment variables:"
echo " MCGHIDRA_PORT - HTTP API port (default: 8192)"
echo " MCGHIDRA_MAXMEM - Max JVM heap (default: 2G)"
echo " PROJECT_NAME - Ghidra project name (default: MCGhidra)"
echo " GHYDRA_PORT - HTTP API port (default: 8192)"
echo " GHYDRA_MAXMEM - Max JVM heap (default: 2G)"
echo " PROJECT_NAME - Ghidra project name (default: GhydraMCP)"
echo " PROJECT_DIR - Project directory (default: /projects)"
echo " GHIDRA_LANGUAGE - Processor language ID (e.g., ARM:LE:32:v4t)"
echo " GHIDRA_BASE_ADDRESS - Base address for raw binaries (e.g., 0x00000000)"
echo " GHIDRA_LOADER - Loader type (e.g., BinaryLoader for raw firmware)"
echo ""
echo "Starting in wait mode..."
echo "Container will stay running for debugging or manual operation."
@ -81,39 +78,9 @@ case "${MCGHIDRA_MODE}" in
-import "${BINARY_PATH}"
-max-cpu 2
-scriptPath "${SCRIPT_DIR}"
-postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}"
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}"
)
# Optional: processor/language for raw binaries
if [ -n "${GHIDRA_LANGUAGE}" ]; then
if ! echo "${GHIDRA_LANGUAGE}" | grep -qE '^[A-Za-z0-9_]+:[A-Z]{2}:[0-9]+:[A-Za-z0-9._-]+$'; then
echo "ERROR: Invalid GHIDRA_LANGUAGE format: ${GHIDRA_LANGUAGE}"
echo "Expected: ARCH:ENDIAN:SIZE:VARIANT (e.g., ARM:LE:32:v4t)"
exit 1
fi
ANALYZE_ARGS+=(-processor "${GHIDRA_LANGUAGE}")
fi
# Optional: base address
if [ -n "${GHIDRA_BASE_ADDRESS}" ]; then
if ! echo "${GHIDRA_BASE_ADDRESS}" | grep -qE '^(0x)?[0-9a-fA-F]+$'; then
echo "ERROR: Invalid GHIDRA_BASE_ADDRESS format: ${GHIDRA_BASE_ADDRESS}"
echo "Expected hex: 0x00000000 or 00000000"
exit 1
fi
ANALYZE_ARGS+=(-loader-baseAddr "${GHIDRA_BASE_ADDRESS}")
fi
# Optional: explicit loader (e.g., BinaryLoader for raw firmware)
if [ -n "${GHIDRA_LOADER}" ]; then
if ! echo "${GHIDRA_LOADER}" | grep -qE '^[A-Za-z0-9_]+$'; then
echo "ERROR: Invalid GHIDRA_LOADER format: ${GHIDRA_LOADER}"
echo "Expected alphanumeric name (e.g., BinaryLoader)"
exit 1
fi
ANALYZE_ARGS+=(-loader "${GHIDRA_LOADER}")
fi
# Add any extra arguments passed
ANALYZE_ARGS+=("$@")
@ -126,10 +93,10 @@ case "${MCGHIDRA_MODE}" in
server)
# Server mode: Open existing project with HTTP server
echo "Starting MCGhidra server on existing project..."
echo "Starting GhydraMCP server on existing project..."
if [ $# -eq 0 ]; then
echo "Usage: docker run -e MCGHIDRA_MODE=server mcghidra [program_name]"
echo "Usage: docker run -e GHYDRA_MODE=server ghydramcp [program_name]"
echo ""
echo " program_name: Name of program in the project to open"
exit 1
@ -143,14 +110,14 @@ case "${MCGHIDRA_MODE}" in
-process "${PROGRAM_NAME}" \
-noanalysis \
-scriptPath "${SCRIPT_DIR}" \
-postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}" \
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}" \
"$@"
;;
analyze)
# Analyze mode: Import and analyze, then exit (no HTTP server)
if [ $# -eq 0 ]; then
echo "Usage: docker run -e MCGHIDRA_MODE=analyze mcghidra [binary_path]"
echo "Usage: docker run -e GHYDRA_MODE=analyze ghydramcp [binary_path]"
exit 1
fi
@ -171,7 +138,7 @@ case "${MCGHIDRA_MODE}" in
;;
*)
echo "Unknown mode: ${MCGHIDRA_MODE}"
echo "Unknown mode: ${GHYDRA_MODE}"
echo "Valid modes: headless, server, analyze, shell"
exit 1
;;

View File

@ -1,6 +0,0 @@
node_modules
dist
.astro
.env
.env.*
!.env.example

View File

@ -1,6 +0,0 @@
node_modules/
dist/
.astro/
.env
.env.local
.env.production

View File

@ -1,61 +0,0 @@
# syntax=docker/dockerfile:1
# -- Build stage --
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
ENV ASTRO_TELEMETRY_DISABLED=1
RUN pnpm build
# -- Production stage --
FROM caddy:2-alpine AS production
COPY --from=builder /app/dist /srv
COPY <<EOF /etc/caddy/Caddyfile
:80 {
root * /srv
encode gzip
try_files {path} {path}/
file_server
header Cache-Control "public, max-age=3600"
@immutable path /_astro/*
header @immutable Cache-Control "public, max-age=31536000, immutable"
handle_errors {
rewrite * /404.html
file_server
}
}
EOF
EXPOSE 80
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
# -- Dev stage --
FROM node:22-alpine AS dev
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
ENV ASTRO_TELEMETRY_DISABLED=1
EXPOSE 4321
CMD ["pnpm", "dev"]

View File

@ -1,13 +0,0 @@
.PHONY: prod dev down logs
prod:
docker compose up -d --build
dev:
docker compose --profile dev up --build
down:
docker compose down
logs:
docker compose logs -f

View File

@ -1,75 +0,0 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
export default defineConfig({
site: 'https://mcghidra.warehack.ing',
devToolbar: { enabled: false },
integrations: [
starlight({
title: 'MCGhidra',
tagline: 'Reverse engineering bridge between Ghidra and MCP',
description: 'Multi-instance Ghidra plugin with HATEOAS REST API and MCP server for decompilation, analysis, and binary manipulation.',
social: [
{ icon: 'external', label: 'Gitea', href: 'https://git.supported.systems/MCP/mcghidra' },
{ icon: 'external', label: 'PyPI', href: 'https://pypi.org/project/mcghidra/' },
],
customCss: ['./src/styles/custom.css'],
editLink: {
baseUrl: 'https://git.supported.systems/MCP/mcghidra/_edit/main/docs-site/',
},
head: [
{
tag: 'meta',
attrs: {
name: 'theme-color',
content: '#c85533',
},
},
],
sidebar: [
{
label: 'Getting Started',
items: [
{ label: 'Overview', slug: 'getting-started/overview' },
{ label: 'Installation', slug: 'getting-started/installation' },
],
},
{
label: 'Guides',
items: [
{ label: 'Analysis Workflows', slug: 'guides/workflows' },
{ label: 'Cursor Pagination', slug: 'guides/cursor-pagination' },
{ label: 'Troubleshooting', slug: 'guides/troubleshooting' },
],
},
{
label: 'Reference',
collapsed: true,
items: [
{ label: 'MCP Tools', slug: 'reference/mcp-tools' },
{ label: 'MCP Resources', slug: 'reference/resources' },
{ label: 'REST API', slug: 'reference/rest-api' },
{ label: 'Configuration', slug: 'reference/configuration' },
{ label: 'Docker Usage', slug: 'reference/docker' },
],
},
{
label: 'Concepts',
collapsed: true,
items: [
{ label: 'Architecture', slug: 'concepts/architecture' },
{ label: 'Prior Art', slug: 'concepts/prior-art' },
],
},
{
label: 'About',
collapsed: true,
items: [
{ label: 'Changelog', slug: 'about/changelog' },
],
},
],
}),
],
});

View File

@ -1,38 +0,0 @@
services:
docs:
build:
context: .
dockerfile: Dockerfile
target: production
restart: unless-stopped
networks:
- caddy
labels:
caddy: ${DOMAIN:-mcghidra.l.warehack.ing}
caddy.reverse_proxy: "{{upstreams 80}}"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
docs-dev:
build:
context: .
dockerfile: Dockerfile
target: dev
profiles: ["dev"]
volumes:
- ./src:/app/src
- ./public:/app/public
- ./astro.config.mjs:/app/astro.config.mjs
networks:
- caddy
labels:
caddy: ${DOMAIN:-mcghidra.l.warehack.ing}
caddy.reverse_proxy: "{{upstreams 4321}}"
networks:
caddy:
external: true

View File

@ -1,24 +0,0 @@
{
"name": "mcghidra-docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev --host",
"start": "astro dev --host",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"@astrojs/starlight": "^0.37.4",
"astro": "^5.6.1",
"sharp": "^0.34.2"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp"
]
}
}

4355
docs-site/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
User-agent: *
Allow: /

View File

@ -1,7 +0,0 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@ -1,88 +0,0 @@
---
title: Changelog
description: Version history and release notes
---
This page summarizes each release. For full commit-level detail, see the repository history.
## Unreleased
### Added
- Symbol CRUD operations: `symbols_create`, `symbols_rename`, `symbols_delete`, `symbols_imports`, `symbols_exports`
- Bookmark management: `bookmarks_list`, `bookmarks_create`, `bookmarks_delete`
- Enum and typedef creation: `enums_create`, `enums_list`, `typedefs_create`, `typedefs_list`
- Variable management: `variables_list`, `variables_rename`, `functions_variables`
- Namespace and class tools: `namespaces_list`, `classes_list`
- Memory segment listing: `segments_list`
- 13 analysis prompts with progress reporting
- Docker port auto-allocation from a configurable pool (default 8192-8223)
- Lazy `instances_use` -- returns immediately, validates on first real call
- All Docker operations non-blocking via thread executors
- Session isolation for `docker_stop` and `docker_cleanup`
### Fixed
- Eliminated 4+ hour hangs when switching to slow or unreachable instances
- Multiple bug fixes across Docker lifecycle and session management
## 2025.12.1
### Added
- Cursor-based pagination with configurable `page_size` and `cursor_id`
- Grep and regex filtering applied before pagination
- 8 enumeration resources using `ghidra://` URIs
### Security
- ReDoS protection on regex filters
- Session spoofing prevention for cursor operations
## 2.0.0
### Changed
- Full MCP integration refactor using FastMCP
- HATEOAS-driven API v2 with hypermedia links on all responses
### Added
- String listing across program memory
- Data manipulation tools
- Cross-reference analysis tools
- Memory read and write operations
## 1.4.0
### Changed
- Communication between bridge and plugin switched to structured JSON
### Added
- Test suites for bridge and plugin
- Origin checking on HTTP requests
## 1.3.0
### Added
- Variable manipulation tools (rename and retype)
- Dynamic version reporting in API responses
## 1.2.0
### Added
- Multi-instance support -- connect to multiple Ghidra sessions and switch between them
## 1.1.0
### Added
- Initial bridge release connecting MCP server to Ghidra plugin
## 1.0.0
- Initial project setup

View File

@ -1,87 +0,0 @@
---
title: Architecture
description: How MCGhidra's components fit together and why
---
MCGhidra is a three-layer stack. Each layer operates independently, communicates over well-defined boundaries, and can be replaced without affecting the others.
```
┌─────────────────────────────────────────────────────────────────┐
│ MCP Client (Claude Code, Claude Desktop, etc.) │
└──────────────────────────┬──────────────────────────────────────┘
│ stdio (MCP protocol)
┌──────────────────────────┴──────────────────────────────────────┐
│ MCGhidra Python Server (FastMCP) │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌────────────┐ │
│ │Functions │ │ Data │ │Analysis │ │ Docker │ ... │
│ │ Mixin │ │ Mixin │ │ Mixin │ │ Mixin │ │
│ └────┬────┘ └────┬─────┘ └────┬────┘ └─────┬─────┘ │
│ └───────────┴────────────┴─────────────┘ │
│ HTTP Client │
└──────────────────────────┬──────────────────────────────────────┘
│ HTTP REST (HATEOAS)
┌──────────────────────────┴──────────────────────────────────────┐
│ Ghidra Plugin (Java, runs inside JVM) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ MCGhidraServer.py → HTTP endpoints │ │
│ │ Functions / Data / Memory / Xrefs / Analysis │ │
│ └────────────────────────────────────────────────────────┘ │
│ Ghidra Analysis Engine (decompiler, disassembler, types) │
└─────────────────────────────────────────────────────────────────┘
```
## The Three-Layer Stack
The top layer is the MCP client. Claude Code, Claude Desktop, or any MCP-compatible tool connects to MCGhidra over stdio using the Model Context Protocol. The client sees MCP tools, resources, and prompts -- it never deals with HTTP or Ghidra's internals directly.
The middle layer is the Python MCP server, built on FastMCP. It translates MCP tool calls into HTTP requests against the Ghidra plugin's REST API. The server is organized as a set of mixins -- Functions, Data, Analysis, Docker, and others -- each registering their own tools. This keeps the codebase navigable despite having 64+ tools.
The bottom layer is the Ghidra plugin. It runs as a Jython script inside Ghidra's JVM and starts an HTTP server that exposes Ghidra's analysis engine. The plugin does not know or care about MCP. It serves a HATEOAS REST API that any HTTP client can consume.
### Why a REST intermediary
A direct JVM-to-MCP bridge sounds simpler, but Ghidra's runtime imposes real constraints. The JVM uses OSGi classloading, the scripting environment is Jython (Python 2.7), and Ghidra's internal APIs are not designed for external consumption. HTTP sidesteps all of this. The Ghidra plugin speaks HTTP; the Python server speaks MCP. Each layer uses the language and runtime best suited to its job.
This separation also enables multi-instance support. Multiple Ghidra processes can run on different ports, each analyzing a different binary, and the MCP server routes requests to the right one. If the REST layer were baked into the MCP transport, this routing would be much harder.
Finally, the REST layer is language-independent. The Python server could be replaced with a Go or Rust implementation without touching the Ghidra plugin. This is not a theoretical benefit -- it means the plugin's API is usable outside of MCP entirely.
## HATEOAS Design
Most REST APIs call themselves RESTful but skip the hypermedia constraint. MCGhidra does not. Every response from the Ghidra plugin includes `_links` pointing to related resources.
A request to `GET /functions/0x401000` returns the function metadata along with links to decompile it, disassemble it, list its variables, and find cross-references. The client follows links rather than constructing URLs from templates.
This matters more for MCP agents than for human users. An agent that follows links does not need to memorize URL patterns or understand the API's URL structure upfront. It reads a response, sees what actions are available, and picks the relevant one. The API is self-describing at every step.
The practical effect: when the Ghidra plugin adds a new capability, the agent can discover and use it without any changes to the MCP server -- as long as the server forwards the link.
## Session Isolation
Each MCP client gets a session ID, derived from the FastMCP context. This ID scopes all stateful operations.
Pagination cursors are session-scoped. If two clients are paging through the same function list, their cursors are independent -- advancing one does not affect the other. Docker containers track which session started them, and `docker_stop` validates ownership before killing a container. One client cannot shut down another client's analysis session.
`docker_cleanup` follows the same rule. It only removes containers and port locks belonging to the calling session, unless explicitly asked to clean up orphans.
## Port Pooling
When Docker provisioning starts a new container, it needs a host port to map the container's HTTP API. Ports come from a configurable pool, defaulting to 8192-8319 (128 ports).
Allocation uses `flock`-based file locking. Each port has a lock file, and the allocator takes an exclusive lock before assigning it. This is safe across multiple processes -- if two MCP servers run on the same host, they will not collide.
The `PortPool` is lazy. It is not created until the first Docker operation that needs a port. If a user never touches Docker, the lock directory is never created and no background work occurs.
A background discovery thread scans the port range every 30 seconds, probing each port with a 0.5-second timeout. This is how the server finds Ghidra instances that were started outside of MCGhidra -- manually launched containers, or Ghidra instances running the plugin natively.
## Non-Blocking Design
The MCP server runs an asyncio event loop. Blocking that loop would freeze all connected clients. MCGhidra avoids this in several ways.
All Docker subprocess calls (`docker run`, `docker stop`, `docker logs`) run in thread pool executors via `asyncio.to_thread`. The event loop stays responsive while containers start, stop, or produce output.
`instances_use` is lazy. When a client switches to a new Ghidra instance, the server creates a stub immediately and returns. It does not validate the connection until the first real tool call against that instance. This avoids the situation where a slow or unreachable Ghidra instance blocks the `instances_use` call for minutes.
`docker_auto_start` returns as soon as the container is running. It does not wait for Ghidra to finish loading and analyzing the binary -- that can take minutes for large files. The client is expected to poll `docker_health` until the API responds.
The background port discovery thread runs on its own schedule and never blocks the event loop. It updates the instance list atomically, so clients always see a consistent snapshot.

View File

@ -1,39 +0,0 @@
---
title: Prior Art
description: Acknowledgments and related projects
---
MCGhidra builds on the work of many people and projects. This page gives credit where it is due.
## Ghidra
NSA released Ghidra as open source in 2019 after years of internal development. MCGhidra would not exist without the decade of investment the agency put into building a full-featured analysis engine. The decompiler alone represents person-years of work on intermediate representations, type inference, and control flow recovery. The fact that it runs headless, supports scripting, and handles dozens of processor architectures out of the box made this project feasible.
Ghidra is available at [ghidra-sre.org](https://ghidra-sre.org/).
## GhidraMCP by Laurie Wired
The direct inspiration. [Laurie Wired's GhidraMCP](https://github.com/LaurieWired/GhidraMCP/) demonstrated that connecting Ghidra to the Model Context Protocol was viable and useful. MCGhidra started as a fork of her project and evolved into a different architecture -- a HATEOAS REST intermediary, multi-instance support, Docker provisioning, cursor-based pagination -- but the core idea of letting an MCP agent drive Ghidra traces back to her work. The proof of concept she built made the case that this was worth pursuing further.
## FastMCP
[FastMCP](https://github.com/jlowin/fastmcp) by Jeremiah Lowin is the Python framework that MCGhidra's MCP server is built on. Its decorator-based tool registration and mixin composition pattern made it practical to organize 64+ tools into maintainable domain modules. The `Context` system for session isolation and progress reporting is central to how MCGhidra handles multi-client scenarios. FastMCP removed a large amount of boilerplate that would otherwise dominate the codebase.
## HATEOAS and REST
The Hypermedia as the Engine of Application State constraint comes from Roy Fielding's 2000 dissertation, where he formalized the REST architectural style. Most APIs that call themselves RESTful ignore this constraint. MCGhidra embraces it because agents benefit from self-describing responses -- when every result includes `_links` to related resources, the agent does not need to memorize URL patterns or maintain a hardcoded API map.
## Model Context Protocol
Anthropic's [MCP specification](https://modelcontextprotocol.io/) provides the transport layer between MCGhidra and its clients. The protocol's tool/resource/prompt abstraction maps naturally to reverse engineering workflows: tools for mutating operations like renaming symbols, resources for read-only enumeration like listing functions, and prompts for guided analysis workflows.
## Related Projects
MCGhidra is part of a broader ecosystem of people bridging reverse engineering tools with external interfaces. Notable related work includes:
- Binary Ninja MCP servers that expose BN's API over the same protocol
- IDA Pro scripting bridges that have connected IDA to external tools for years
- Radare2 and rizin automation frameworks, which pioneered the idea of a scriptable RE command interface
- The growing community of MCP server authors connecting domain-specific tools to language model agents
Each of these projects informed the design decisions in MCGhidra, whether by example or by contrast. The RE tooling community has a long history of building bridges between analysis engines and the outside world -- MCGhidra is one more entry in that tradition.

View File

@ -1,99 +0,0 @@
---
title: Installation
description: Installing MCGhidra from PyPI and configuring your MCP client
---
## Install from PyPI
```bash
pip install mcghidra
```
Or with [uv](https://docs.astral.sh/uv/) for isolated execution:
```bash
uvx mcghidra
```
This installs the MCP server and bundles the Ghidra plugin JAR. No separate plugin installation is needed — the server deploys the JAR automatically when starting Docker containers.
## MCP Client Configuration
### Claude Desktop
Add to your Claude Desktop configuration file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"mcghidra": {
"command": "uvx",
"args": ["mcghidra"]
}
}
}
```
### Claude Code
```bash
claude mcp add mcghidra -- uvx mcghidra
```
Or if running from a local clone:
```bash
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
```
## Docker Setup (Optional)
If you want automatic container provisioning:
1. Clone the repository:
```bash
git clone https://git.supported.systems/MCP/mcghidra.git
cd mcghidra
```
2. Build the Docker image:
```bash
cd docker && docker build -t mcghidra:latest -f Dockerfile ..
```
Or use the MCP tool:
```
docker_build()
```
3. The `docker_auto_start` tool handles everything else — port allocation, container naming, health polling.
## Analyzing Raw Firmware
For raw binary blobs (firmware dumps, bootloaders), specify the processor architecture:
```
docker_auto_start(
binary_path="/path/to/firmware.bin",
language="ARM:LE:32:v4t",
base_address="0x00000000"
)
```
Common language IDs:
| Target | Language ID |
|--------|------------|
| ARM7TDMI (Thumb) | `ARM:LE:32:v4t` |
| ARM Cortex-M | `ARM:LE:32:Cortex` |
| ARMv7 | `ARM:LE:32:v7` |
| MIPS32 LE | `MIPS:LE:32:default` |
| MIPS32 BE | `MIPS:BE:32:default` |
| x86 32-bit | `x86:LE:32:default` |
| x86-64 | `x86:LE:64:default` |
When `language` is specified, the loader is automatically set to `BinaryLoader` (raw binary import). Override with `loader="AutoImporter"` if your file has a recognized header format.

View File

@ -1,69 +0,0 @@
---
title: Overview
description: What MCGhidra does and how the pieces fit together
---
MCGhidra is a two-part system that bridges NSA's [Ghidra](https://ghidra-sre.org/) reverse engineering framework with [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) clients.
## What You Get
- **64 MCP tools** across 12 categories (functions, data, structs, symbols, analysis, memory, variables, bookmarks, enums, namespaces, segments, Docker)
- **13 analysis prompts** for guided RE workflows (malware triage, crypto identification, auth bypass hunting, protocol analysis, and more)
- **19 MCP resources** for quick enumeration without tool calls
- **Cursor-based pagination** for handling binaries with 100K+ functions
- **Server-side grep** filtering before results hit the wire
- **Docker provisioning** with automatic port pooling and session isolation
## Components
### Ghidra Plugin (Java)
A headless-compatible Ghidra script that starts an HTTP server inside the JVM. The server exposes a HATEOAS REST API — every response includes hypermedia links to related resources, so clients discover the API by following links rather than memorizing paths.
The plugin supports:
- Function listing, decompilation, and disassembly
- Memory reads and data type inspection
- Symbol renaming and annotation
- Cross-reference navigation
- Program metadata and analysis status
- Health checks for container orchestration
### MCP Server (Python)
A [FastMCP](https://github.com/jlowin/fastmcp) server that wraps the REST API as MCP tools. It adds:
- **Multi-instance management** — connect to multiple Ghidra sessions, switch between them
- **Docker provisioning** — automatic container lifecycle with port pooling
- **Raw firmware support** — specify processor language, base address, and loader for binary blobs
- **Session isolation** — each MCP client gets its own session ID, preventing cross-talk
## How the Pieces Connect
```
┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
│ MCP Client │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
│ (Claude, │ stdio │ Python │ REST API │ Plugin │
│ Cursor, │ │ Server │ (HATEOAS) │ (Java) │
│ etc.) │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
┌─────┴─────┐
│ Docker │
│ Engine │
└───────────┘
```
The MCP client communicates with MCGhidra over stdio using the Model Context Protocol. MCGhidra translates tool calls into HTTP requests against the Ghidra plugin's REST API. When Docker is available, MCGhidra can also provision and manage Ghidra containers automatically.
## Typical Workflow
1. **Start Ghidra** — either via Docker (`docker_auto_start`) or by running the plugin in an existing Ghidra instance
2. **Connect** — the MCP server discovers the Ghidra instance via health checks
3. **Analyze** — use MCP tools to decompile functions, read memory, navigate references
4. **Iterate** — rename symbols, annotate findings, re-decompile to see updated output
## Requirements
- Python 3.11+
- Ghidra 11.0+ (for the plugin)
- Docker (optional, for container management)

View File

@ -1,158 +0,0 @@
---
title: Cursor Pagination
description: Working with large binaries using cursor-based pagination
---
Large binaries can contain tens of thousands of functions, hundreds of thousands of cross-references, and thousands of strings. Returning all of that in a single tool response would overflow the MCP client's context window and produce unusable output. MCGhidra uses cursor-based pagination to deliver results in controlled pages.
## How it works
When a paginated tool returns more items than the page size, the response includes a `cursor_id`. Pass that cursor ID to `cursor_next` to get the next page. Continue until `has_more` is `false`.
```
# First page: get 100 functions matching a pattern
result = functions_list(page_size=100, grep="crypt|hash")
# Returns:
# {
# result: [...],
# pagination: {
# cursor_id: "a1b2c3d4e5f67890",
# total_count: 12847,
# filtered_count: 847,
# page_size: 100,
# current_page: 1,
# total_pages: 9,
# has_more: true
# }
# }
# Next page
result = cursor_next(cursor_id="a1b2c3d4e5f67890")
# Returns page 2 of 9
# Continue until has_more is false
result = cursor_next(cursor_id="a1b2c3d4e5f67890")
# ...
```
Each response also includes a `_message` field with a human-readable summary like "Showing 100 of 847 items (page 2/9). To get the next 100 items, call: cursor_next(cursor_id='a1b2c3d4e5f67890')". MCP clients use this to decide whether to continue fetching.
## Server-side grep filtering
The `grep` parameter filters results on the server before pagination. This is much more efficient than fetching everything and filtering client-side, because only matching items are stored in the cursor and counted toward page totals.
```
# Only functions with "auth" or "login" in their name/address
functions_list(grep="auth|login", page_size=50)
# Case-sensitive search (grep_ignorecase defaults to true)
data_list_strings(grep="BEGIN CERTIFICATE", grep_ignorecase=false, page_size=50)
```
The grep pattern is a regular expression. It matches against all string values in each result item -- for a function, that means the name, address, and signature fields are all searched.
### Pattern safety
Patterns are validated before execution to prevent runaway matches:
- Maximum 500 characters
- Maximum 15 repetition operators (`*`, `+`, `?`, `{n,m}`)
- Nested quantifiers like `(a+)+` are rejected
If a pattern fails validation, the tool returns an error with code `INVALID_GREP_PATTERN` explaining what to fix.
## The return_all option
When you need all matching results without paging through cursors, pass `return_all=True`:
```
functions_list(grep="crypt", return_all=True)
```
This bypasses pagination and returns every matching item in a single response. There is a token budget guard (default: 8,000 estimated tokens) that kicks in if the response would be too large. When the guard triggers, the response includes:
- A sample of the first 3 items
- The available field names
- Suggested narrower queries (grep patterns, field projections, or pagination)
Combine `return_all` with `grep` and `fields` to keep the response size down:
```
# Get all crypto-related function names and addresses (nothing else)
functions_list(grep="crypt|aes|sha", fields=["name", "address"], return_all=True)
```
## Page size
The `page_size` parameter controls how many items each page contains.
| Parameter | Default | Maximum |
|-----------|---------|---------|
| `page_size` | 50 | 500 |
For most MCP client contexts, 50-100 items per page is a good balance between making progress and keeping individual responses readable. Going above 200 is rarely useful unless you are scripting.
## Cursor lifecycle
### TTL and eviction
Cursors expire after 5 minutes of inactivity (no `cursor_next` calls). The timer resets each time a cursor is accessed.
When more than 100 cursors exist for a session, the least-recently-used cursor is evicted to make room. In practice, you will rarely hit this limit unless you start many queries without finishing them.
### Session isolation
Each MCP client session gets its own set of cursors. You cannot access or interfere with another session's cursors. Session IDs are derived from the MCP client context -- they are not user-controllable.
### Management tools
| Tool | What it does |
|------|-------------|
| `cursor_list()` | Show all active cursors for the current session: IDs, page progress, TTL remaining, grep pattern |
| `cursor_delete(cursor_id="...")` | Delete a specific cursor to free memory |
| `cursor_delete_all()` | Delete all cursors for the current session |
These are useful for cleanup during long analysis sessions or when you want to re-run a query from scratch.
## Example: scanning all strings for credentials
```
# Start with a broad credential search
result = data_list_strings(grep="password|secret|key|token|api_key|credential", page_size=100)
# Process first page of results
# ... examine the strings ...
# Get more if there are additional pages
if result has cursor_id:
result = cursor_next(cursor_id="...")
```
## Example: iterating through all functions matching a pattern
```
# First page
result = functions_list(grep="handle_|process_|parse_", page_size=50)
# Loop through pages
while result has cursor_id:
# Decompile interesting functions from this page
for func in result:
if func looks relevant:
functions_decompile(name=func["name"])
# Advance
result = cursor_next(cursor_id="...")
```
## Tips
- Prefer server-side `grep` over fetching everything. A query like `functions_list(grep="ssl")` is far cheaper than `functions_list(return_all=True)` followed by manual filtering.
- Use `fields` to reduce response size. If you only need names and addresses, `functions_list(fields=["name", "address"], page_size=100)` cuts the per-item size significantly.
- Small page sizes (50-100) keep individual responses from consuming too much context. You can always fetch more pages.
- If a cursor expires (5-minute TTL), just re-run the original query. The cursor IDs are not reusable -- you get a new one each time.
- For very large binaries (100K+ functions), start with grep-filtered queries rather than listing everything. Even paginated, iterating through 2,000 pages of 50 items each is slow and rarely what you actually need.

View File

@ -1,257 +0,0 @@
---
title: Troubleshooting
description: Common issues and solutions when using MCGhidra
---
## Container Issues
### Container will not start
Check that the binary path is correct and accessible from the Docker daemon. The path you pass to `docker_auto_start` must exist on the host machine, and the Docker volume mount must be able to reach it.
```
docker_auto_start(binary_path="/path/to/binary")
```
If this fails, verify:
- The file exists at the specified path
- The `mcghidra:latest` Docker image is built (run `docker_status()` to check)
- Docker is running and your user has permission to access it
### Health check timeouts
Analysis takes time. A small binary (under 1 MB) typically finishes in about 20 seconds. Larger binaries -- especially firmware images or complex C++ programs -- can take several minutes.
Poll `docker_health` to check readiness:
```
docker_health(port=8195)
```
While waiting, check what Ghidra is doing:
```
docker_logs(port=8195)
```
If you see Ghidra import and analysis messages in the logs but the health check never succeeds, the analysis is still running. If the logs show errors or the container has exited, the import likely failed (see "Import failed" below).
### Port conflicts
MCGhidra allocates ports from a pool (default 8192-8319). If another application is using a port in this range, the allocator skips it. If you run many concurrent containers and exhaust the pool, `docker_auto_start` will report that no ports are available.
Check current allocations with:
```
docker_status()
```
You can adjust the port range with environment variables:
| Variable | Default |
|----------|---------|
| `MCGHIDRA_PORT_START` | `8192` |
| `MCGHIDRA_PORT_END` | `8319` |
### Viewing container logs
```
docker_logs(port=8195, tail=200)
```
This shows stdout and stderr from the Ghidra headless process. Look for lines containing `ERROR`, `WARN`, or `Exception` to diagnose import or analysis failures.
---
## Connection Issues
### "No Ghidra instance specified"
This means no current instance is set. First, discover available instances, then select one:
```
instances_list()
instances_use(port=8195)
```
If `instances_list` returns no instances, either no Ghidra process is running or it is on a port outside the discovery range.
### Instance not found after starting a container
`docker_auto_start` returns a port, but the MCP server does not automatically register it as the current instance. You need to call:
```
instances_use(port=8195)
```
If `instances_list` does not show the container, the API may not be ready yet. Poll `docker_health` first.
### API version mismatch
If you see version mismatch errors, the Ghidra plugin is older than the MCP server expects. The current server expects API v2. Update the plugin by rebuilding the Docker image or installing the latest MCGhidra release.
### Timeout on first tool call after instances_use
`instances_use` is lazy -- it creates a stub entry without connecting to Ghidra. The first real tool call (like `functions_list`) validates the connection. If Ghidra is not ready yet, that call will time out.
Wait for `docker_health` to report healthy before calling `instances_use`.
---
## Analysis Issues
### Import failed
Raw binaries (firmware, bootloaders) need the `language` parameter to tell Ghidra which processor architecture to use. Without it, Ghidra tries to auto-detect the format and will fail on headerless files.
```
docker_auto_start(
binary_path="/path/to/firmware.bin",
language="ARM:LE:32:v4t",
base_address="0x00000000"
)
```
Check the logs if auto-import fails:
```
docker_logs(port=8195)
```
Common causes:
- Missing `language` for raw binaries
- Incorrect base address
- Corrupted or truncated binary file
- Unsupported file format (check with the `file` command on the host)
### OSGi bundle error
This is a known Ghidra limitation that can occur with certain script configurations. It appears as "Failed to get OSGi bundle" in the container logs. It does not usually affect analysis results -- the API still functions. If it blocks operation, rebuilding the Docker image with the latest scripts resolves it in most cases.
### Analysis incomplete
If decompiled output looks wrong (missing function boundaries, incorrect types), Ghidra's auto-analysis may not have finished or may need a second pass:
```
analysis_run()
```
This triggers a full re-analysis of the current program. It can take a while on large binaries.
### Decompilation timeout
For very large or complex functions, the decompiler can take longer than the default timeout. If `functions_decompile` times out, the function may have deeply nested loops, heavy inlining, or obfuscated control flow.
Try disassembly instead for a faster view:
```
functions_disassemble(address="00401234")
```
---
## Pagination Issues
### Cursor expired
Cursors have a 5-minute inactivity TTL. If you wait too long between `cursor_next` calls, the cursor is deleted. Re-run the original query to get a fresh cursor:
```
functions_list(grep="crypt", page_size=100)
```
See [Cursor Pagination](/guides/cursor-pagination/) for details on cursor lifecycle.
### Context window overflow
If tool responses are consuming too much context, reduce the page size:
```
functions_list(page_size=25, grep="your_pattern")
```
Use `fields` to limit which fields are returned:
```
functions_list(page_size=50, fields=["name", "address"])
```
And always prefer `grep` to filter results before they reach the client.
### "Session spoofing" errors
Session IDs are derived from the MCP client context and cannot be set manually. If you see session-related errors, it means a cursor belongs to a different MCP session. Each session (each Claude conversation, for example) has its own isolated cursor space.
---
## Docker-Specific Issues
### docker_auto_start appears to hang
`docker_auto_start` returns immediately after starting the container. It does not wait for analysis to complete. If it seems to hang, the issue is likely Docker itself taking time to pull or start the container. Check:
```
docker_status()
```
### Cross-session interference
Each MCP session has a unique session ID. Docker containers are tagged with their owning session. `docker_stop` validates that the container belongs to your session before stopping it. You cannot stop another session's container.
If you need to clean up containers from a previous session that is no longer active, use:
```
docker_cleanup(session_only=False)
```
Be careful with this -- it removes all orphaned MCGhidra containers, not just yours.
### Stale containers
If containers from previous sessions are still running, they consume ports from the pool. Use `docker_cleanup()` (which defaults to `session_only=True`) to clean up your own stale containers, or `docker_cleanup(session_only=False)` to remove all orphaned containers.
### Build failures
If `docker_build()` fails, make sure:
- The Dockerfile context is correct (it needs both the `docker/` directory and the project root)
- Docker has enough disk space
- The base Ghidra image layers download successfully (network access required for first build)
---
## Debug Mode
Set the `MCGHIDRAMCP_DEBUG` environment variable before starting the MCP server to enable verbose logging:
```bash
MCGHIDRAMCP_DEBUG=1 uvx mcghidra
```
Or in your MCP client configuration:
```json
{
"mcpServers": {
"mcghidra": {
"command": "uvx",
"args": ["mcghidra"],
"env": {
"MCGHIDRAMCP_DEBUG": "1"
}
}
}
}
```
Debug output goes to stderr and includes:
- Instance discovery attempts and results
- HTTP request/response details for Ghidra API calls
- Cursor creation, access, and expiration events
- Docker container lifecycle events
- Port pool allocation and release
Check the MCP server's stderr output in your terminal or in the MCP client's server log viewer.

View File

@ -1,236 +0,0 @@
---
title: Analysis Workflows
description: Common reverse engineering workflows with MCGhidra
---
These workflows assume you have MCGhidra installed and configured as described in the [Installation guide](/getting-started/installation/).
## Triage a Binary
The fastest way to get oriented in an unknown binary. Start a container, wait for Ghidra to finish analysis, then survey the surface area.
### 1. Start analysis
```
docker_auto_start(binary_path="/path/to/target.exe")
```
This returns immediately with a port number. It does not block while Ghidra runs.
### 2. Wait for analysis to complete
Poll until the HTTP API responds:
```
docker_health(port=8195)
```
For a small binary (under 1 MB), expect about 20 seconds. Larger binaries can take several minutes. Check `docker_logs(port=8195)` while waiting to see Ghidra's progress.
### 3. Set the instance as current
```
instances_use(port=8195)
```
After this, every tool call defaults to this instance. No need to pass `port` again.
### 4. Get the program overview
```
program_info()
functions_list(page_size=100)
data_list_strings(page_size=100)
```
`program_info` returns architecture, compiler, and image base address. The function and string listings give a first sense of scale and naming conventions.
### 5. Search for interesting patterns
Use server-side grep to find functions and strings related to security-sensitive behavior:
```
functions_list(grep="password|key|auth|crypt|login|verify", page_size=100)
data_list_strings(grep="password|secret|key|token|credential", page_size=100)
```
From here, decompile anything that looks relevant and follow cross-references to understand the surrounding logic.
---
## Rename and Annotate Loop
Ghidra auto-analysis produces generic names like `FUN_00401234`. As you reverse engineer, renaming functions and adding comments makes the decompiled output progressively easier to read.
### 1. Decompile a function
```
functions_decompile(address="00401234")
```
### 2. Identify what it does
Read the pseudocode. Look at string references, called functions, and parameter usage to determine the function's purpose.
### 3. Rename it
```
functions_rename(address="00401234", new_name="validate_user_credentials")
```
### 4. Set the signature
If you can determine the parameter types and return type:
```
functions_set_signature(
address="00401234",
signature="int validate_user_credentials(char *username, char *password)"
)
```
### 5. Add a comment
```
functions_set_comment(
address="00401234",
comment="Checks username/password against the SQLite user table. Returns 1 on success."
)
```
### 6. Re-decompile
```
functions_decompile(address="00401234")
```
The decompiled output now uses your names, types, and annotations. Functions called from `validate_user_credentials` also reflect the updated name wherever they reference it. Repeat this loop for each function you investigate.
---
## Firmware Reverse Engineering
Raw firmware (bootloaders, embedded system images, bare-metal code) requires extra setup because there is no ELF/PE header for Ghidra to parse.
### 1. Start with the right loader
Specify the processor language and base address:
```
docker_auto_start(
binary_path="/path/to/firmware.bin",
language="ARM:LE:32:v4t",
base_address="0x00000000"
)
```
When `language` is set, MCGhidra uses `BinaryLoader` to map the raw bytes at the given address. See the [Installation guide](/getting-started/installation/) for a table of common language IDs.
### 2. Find the entry point
ARM firmware typically starts with an exception vector table at address 0x00000000. The first entry is the initial stack pointer, and the second is the reset vector (entry point):
```
memory_read(address="0x00000000", length=32, format="hex")
functions_decompile(address="0x00000004")
```
### 3. Identify peripherals
Embedded firmware talks to hardware through memory-mapped I/O. Look for reads and writes to addresses outside the firmware's code and data regions:
```
data_list_strings(grep="UART|SPI|I2C|GPIO")
functions_list(grep="init_periph|hw_init|bsp_")
```
Constants like `0x40000000`, `0x48000000`, or `0xE000E000` (ARM Cortex-M NVIC) are strong indicators of peripheral access.
### 4. Trace interrupt handlers
Interrupt vector tables are typically at fixed offsets. For Cortex-M, the vector table starts at the base address. Each 4-byte entry points to a handler:
```
memory_read(address="0x00000000", length=256, format="hex")
```
Create functions at each non-null vector address:
```
functions_create(address="0x00000040")
functions_decompile(address="0x00000040")
```
### 5. Map protocol implementations
Firmware that communicates over a bus (UART, SPI, USB, CAN) will have recognizable patterns: ring buffers, state machines with packet parsing, and checksum calculations. Use call graph analysis to trace from peripheral init functions to protocol handlers:
```
analysis_get_callgraph(name="uart_init", max_depth=4)
```
---
## Using Analysis Prompts
MCGhidra includes 13 built-in prompts that guide Claude through structured analysis workflows. Each prompt defines a series of steps, tool calls, and checks for a specific reverse engineering task.
### Running a prompt
In Claude Code or Claude Desktop, use the `/prompt` command:
```
/prompt malware_triage
```
Claude will then execute a multi-step analysis: listing functions, scanning strings, checking imports, and producing a structured report. Prompts that involve scanning (like `malware_triage` or `identify_crypto`) report progress as they work through each step.
### Available prompts
| Prompt | What it does |
|--------|-------------|
| `malware_triage` | Quick capability assessment across 21 scanning steps: checks for network activity, file manipulation, process injection, anti-analysis tricks, and persistence mechanisms |
| `identify_crypto` | Scans for known crypto constants (AES S-boxes, SHA magic numbers), function names matching crypto libraries, and common key schedule patterns |
| `find_authentication` | Searches for password checks, credential storage, license validation, certificate handling, and authentication bypass patterns |
| `analyze_protocol` | Framework for reversing network or file format protocols: identifies packet structures, state machines, serialization routines |
| `trace_data_flow` | Follows data forward or backward through a program to map how input reaches sensitive operations |
| `find_main_logic` | Navigates past CRT startup, compiler-generated wrappers, and initialization to find the actual application entry point |
| `analyze_imports` | Categorizes imported functions by capability (file I/O, networking, crypto, process management) and flags suspicious combinations |
| `analyze_strings` | Groups strings by category (URLs, file paths, error messages, format strings) and cross-references them to find their usage |
| `analyze_switch_table` | Identifies jump tables and command dispatchers, maps case values to handler functions |
| `find_config_parsing` | Locates configuration file readers, command-line parsers, registry access, and environment variable lookups |
| `compare_functions` | Side-by-side comparison of two functions to identify patches, variants, or shared library code |
| `document_struct` | Traces struct usage across the binary to document field types, offsets, sizes, and purpose |
| `find_error_handlers` | Maps error handling paths, cleanup routines, exception handlers, and exit patterns |
### Prompt examples
Triage an unknown binary for malicious capabilities:
```
/prompt malware_triage
```
Find all cryptographic implementations:
```
/prompt identify_crypto
```
Trace how user input flows to a specific sink:
```
/prompt trace_data_flow
```
### What happens during a prompt
Each prompt orchestrates a series of MCP tool calls. For example, `malware_triage` will:
1. Call `program_info()` to determine the architecture and format
2. Call `functions_list(grep=...)` repeatedly with patterns for each capability category (networking, file ops, process injection, etc.)
3. Call `data_list_strings(grep=...)` to find suspicious string patterns
4. Call `symbols_imports(grep=...)` to categorize imported APIs
5. Produce a summary with findings organized by risk category
Prompts that scan many patterns report numeric progress (e.g., "Step 12/21: Checking for anti-debug techniques") so you can see where they are in the analysis.

View File

@ -1,67 +0,0 @@
---
title: MCGhidra
description: Reverse engineering bridge between Ghidra and MCP
template: splash
hero:
tagline: Multi-instance Ghidra with a REST API and MCP server for decompilation, analysis, and binary manipulation.
actions:
- text: Getting Started
link: /getting-started/overview/
icon: right-arrow
- text: View on PyPI
link: https://pypi.org/project/mcghidra/
icon: external
variant: minimal
---
import { Card, CardGrid } from '@astrojs/starlight/components';
## What is MCGhidra?
MCGhidra connects [Ghidra](https://ghidra-sre.org/) to the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), turning Ghidra's analysis engine into a set of tools that any MCP client can use. It consists of two parts: a **Ghidra plugin** that exposes a HATEOAS REST API from within Ghidra's JVM, and a **Python MCP server** that translates those endpoints into MCP tools.
The result is that an MCP client can decompile functions, read memory, rename symbols, navigate cross-references, and manage Docker-based Ghidra instances — all through standard tool calls.
<CardGrid>
<Card title="HATEOAS REST API" icon="puzzle">
The Ghidra plugin serves a discoverable API with hypermedia links. Clients navigate resources by following links rather than hardcoding endpoint paths.
</Card>
<Card title="Multi-Instance" icon="list-format">
Run multiple Ghidra sessions in parallel, each analyzing a different binary. The MCP server tracks instances and routes requests to the right one.
</Card>
<Card title="Docker Support" icon="rocket">
Automatic container provisioning with port pooling. Pass a binary path and MCGhidra handles image building, container startup, and health polling.
</Card>
<Card title="Raw Firmware" icon="setting">
Specify processor architecture, base address, and loader type for raw firmware binaries. Supports targets like ARM7TDMI, MIPS, x86, and anything Ghidra recognizes.
</Card>
</CardGrid>
## Quick Install
```bash
# Install the MCP server
pip install mcghidra
# Or use uvx for isolated execution
uvx mcghidra
```
The Ghidra plugin JAR ships inside the Python package and is automatically deployed to running Ghidra instances or Docker containers.
## Architecture
```
MCP Client (Claude, etc.)
MCGhidra MCP Server (Python, FastMCP)
Ghidra Plugin REST API (Java, runs inside Ghidra's JVM)
Ghidra Analysis Engine
```
Each layer is independently useful. The REST API works without MCP. The MCP server works without Docker. Docker support is optional for automated container management.

View File

@ -1,115 +0,0 @@
---
title: Configuration
description: Environment variables and settings for the MCP server, Docker containers, and port pool
---
MCGhidra is configured through environment variables. No configuration file is required -- defaults work out of the box for local development.
## MCP Server
These variables control the Python MCP server process.
| Variable | Default | Description |
|----------|---------|-------------|
| `GHIDRA_HOST` | `localhost` | Hostname for connecting to Ghidra instances. Change this when Ghidra runs on a remote host. |
| `MCGHIDRAMCP_DEBUG` | *unset* | Set to `1` to enable DEBUG-level logging. Shows HTTP requests, pagination details, and discovery results. |
| `MCGHIDRA_FEEDBACK` | `true` | Enable or disable feedback collection. Set to `false` to disable. |
| `MCGHIDRA_FEEDBACK_DB` | `~/.mcghidra/feedback.db` | Path to the SQLite database for feedback data. The parent directory is created automatically. |
### Internal Defaults
These values are set in `MCGhidraConfig` and are not currently exposed as environment variables, but can be overridden programmatically when creating the server:
| Setting | Default | Description |
|---------|---------|-------------|
| `quick_discovery_range` | 18489-18498 | Port range for quick instance discovery scans |
| `full_discovery_range` | 18400-18599 | Port range for full discovery scans (`instances_discover`) |
| `request_timeout` | 30.0s | HTTP request timeout for Ghidra API calls |
| `discovery_timeout` | 0.5s | HTTP timeout per port during discovery scans |
| `default_page_size` | 50 | Default pagination page size |
| `max_page_size` | 500 | Maximum allowed page size |
| `cursor_ttl_seconds` | 300 | Cursor expiration time (5 minutes) |
| `max_cursors_per_session` | 100 | Maximum active cursors per MCP session |
| `max_response_tokens` | 8000 | Hard token budget -- the return_all guard triggers above this |
| `expected_api_version` | 2 | Minimum API version required from the Ghidra plugin |
---
## Docker Image
These variables control the MCP server's Docker integration -- how it builds, tags, and starts containers.
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRAMCP_VERSION` | `latest` | Docker image tag to use when starting containers. |
| `MCGHIDRA_PORT` | `8192` | Default port for container API mapping. Overridden by auto-allocation in multi-container mode. |
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size passed to containers. Increase for large binaries. |
| `MCGHIDRA_DOCKER_AUTO` | `false` | When `true`, the server will automatically start a Docker container when a binary is loaded and no Ghidra instance is available. |
---
## Port Pool
The port pool prevents conflicts when multiple MCP sessions run containers simultaneously. Ports are allocated using `flock`-based locking.
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRA_PORT_START` | `8192` | First port in the allocation pool. |
| `MCGHIDRA_PORT_END` | `8319` | Last port in the allocation pool (128 ports total). |
| `MCGHIDRA_PORT_LOCK_DIR` | `/tmp/mcghidra-ports` | Directory for port lock files. Created automatically on first use. |
Port lock files are named `port-{N}.lock` and contain JSON with the session ID, PID, and timestamp. The `docker_cleanup` tool removes stale locks from crashed processes.
---
## Container Environment
These variables are read by the Docker entrypoint script (`entrypoint.sh`) inside the container. They configure how Ghidra runs in headless mode.
### Core Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRA_MODE` | `headless` | Container operating mode. See modes below. |
| `MCGHIDRA_PORT` | `8192` | HTTP API port inside the container. The MCP server maps this to a host port from the pool. |
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size. Passed to Ghidra's `analyzeHeadless` command. |
### Ghidra Paths
| Variable | Default | Description |
|----------|---------|-------------|
| `GHIDRA_HOME` | `/opt/ghidra` | Ghidra installation directory inside the container. |
| `SCRIPT_DIR` | `/home/ghidra/ghidra_scripts` | Directory for Ghidra Python scripts (MCGhidraServer.py lives here). |
| `PROJECT_DIR` | `/projects` | Directory where Ghidra stores its project files (.gpr, .rep). |
| `PROJECT_NAME` | `MCGhidra` | Name of the Ghidra project created for the imported binary. |
### Firmware Import Options
These are optional. When omitted, Ghidra auto-detects the binary format.
| Variable | Default | Description |
|----------|---------|-------------|
| `GHIDRA_LANGUAGE` | *auto-detect* | Processor language ID. Must match `ARCH:ENDIAN:SIZE:VARIANT` format (e.g., `ARM:LE:32:v4t`). Setting this causes the container to use `BinaryLoader` unless `GHIDRA_LOADER` overrides it. |
| `GHIDRA_BASE_ADDRESS` | *auto-detect* | Base address for raw binaries. Hex format: `0x00000000` or `00000000`. |
| `GHIDRA_LOADER` | *auto-detect* | Loader type override. Common values: `BinaryLoader` (raw bytes), `AutoImporter` (header-based detection). Must be alphanumeric with underscores. |
### Container Modes
The `MCGHIDRA_MODE` variable selects the operating mode:
| Mode | Description |
|------|-------------|
| `headless` | Default. Imports the binary, runs auto-analysis, starts the HTTP API server. This is what `docker_auto_start` and `docker_start` use. |
| `server` | Opens an existing project (no import). Requires a program name as an argument. Useful for re-analyzing a previously imported binary. |
| `analyze` | Imports and analyzes a binary, then exits. No HTTP server. Use for batch processing. |
| `shell` | Drops into an interactive bash shell. Useful for debugging the container environment. |
### Validation
All firmware import parameters are validated before reaching Ghidra:
- `GHIDRA_LANGUAGE` must match `ARCH:ENDIAN:SIZE:VARIANT` (regex-validated).
- `GHIDRA_BASE_ADDRESS` must be valid hex, max 64-bit.
- `GHIDRA_LOADER` must be alphanumeric with underscores.
Invalid values are rejected with a descriptive error before any Docker or Ghidra operations run. The MCP server validates these on the client side as well, so errors surface in tool responses rather than buried in container logs.

View File

@ -1,87 +0,0 @@
---
title: Docker Usage
description: Container management, port pooling, and firmware import options
---
MCGhidra can automatically provision Docker containers running Ghidra in headless mode. Each container gets a dedicated port from a configurable pool, and containers are tracked by session to prevent cross-talk between concurrent MCP clients.
## Quick Start
```
docker_auto_start(binary_path="/path/to/binary")
```
This checks for an existing instance analyzing the same binary, and if none is found, starts a new container with an auto-allocated port.
## Environment Variables
The Docker entrypoint accepts these environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRA_PORT` | `8192` | HTTP API port inside the container |
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size |
| `PROJECT_NAME` | `MCGhidra` | Ghidra project name |
| `PROJECT_DIR` | `/projects` | Project directory inside container |
| `GHIDRA_LANGUAGE` | *(auto-detect)* | Processor language ID (e.g., `ARM:LE:32:v4t`) |
| `GHIDRA_BASE_ADDRESS` | *(auto-detect)* | Base address for raw binaries (e.g., `0x00000000`) |
| `GHIDRA_LOADER` | *(auto-detect)* | Loader type (e.g., `BinaryLoader`) |
## Port Pool
Ports are allocated from a pool to prevent conflicts:
| Variable | Default | Description |
|----------|---------|-------------|
| `MCGHIDRA_PORT_START` | `8192` | First port in the pool |
| `MCGHIDRA_PORT_END` | `8319` | Last port in the pool (128 ports) |
| `MCGHIDRA_PORT_LOCK_DIR` | `/tmp/mcghidra-ports` | Lock file directory |
Port allocation uses `flock`-based locking for cross-process safety.
## Raw Firmware Import
For binaries without recognized headers (raw firmware dumps, bootloader images):
```
docker_start(
binary_path="/path/to/firmware.bin",
language="ARM:LE:32:v4t",
base_address="0x00000000"
)
```
When `language` is specified, `loader` is automatically set to `BinaryLoader`. This tells Ghidra to treat the file as raw bytes mapped at the given base address, rather than trying to parse it as an ELF, PE, or Mach-O.
To override the auto-loader (e.g., if you have an ELF but need a specific language):
```
docker_start(
binary_path="/path/to/firmware.elf",
language="ARM:LE:32:v7",
loader="AutoImporter"
)
```
## Container Lifecycle
| Tool | Description |
|------|-------------|
| `docker_auto_start` | Find existing or start new container |
| `docker_start` | Start a container explicitly |
| `docker_stop` | Stop and remove a container |
| `docker_health` | Check if API is responding |
| `docker_logs` | View container output |
| `docker_status` | List all containers and images |
| `docker_cleanup` | Remove orphaned containers and stale locks |
| `docker_session_info` | Show this session's containers |
## Input Validation
All firmware import parameters are validated before reaching the container:
- **Language**: Must match `ARCH:ENDIAN:SIZE:VARIANT` pattern (e.g., `ARM:LE:32:v4t`)
- **Base address**: Must be a valid hex string (e.g., `0x00000000` or `00000000`), max 64-bit
- **Loader**: Must be alphanumeric with underscores (e.g., `BinaryLoader`)
Invalid values are rejected with a descriptive error before any Docker operations occur.

View File

@ -1,742 +0,0 @@
---
title: MCP Tools
description: Complete reference for all MCGhidra MCP tools, grouped by domain
---
MCGhidra exposes Ghidra's capabilities as MCP tools. There are 64 tools across 14 categories.
## Pagination Convention
Most list tools share a common set of pagination and filtering parameters. Rather than repeating them in every table, they are documented once here:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *current* | Ghidra instance port. Uses the active instance if omitted. |
| `page_size` | int | `50` | Items per page. Maximum: 500. |
| `grep` | string | *none* | Client-side regex pattern applied to results after fetching. |
| `grep_ignorecase` | bool | `true` | Case-insensitive grep matching. |
| `return_all` | bool | `false` | Bypass pagination and return everything. Triggers a budget guard if the response exceeds ~8000 tokens. |
| `fields` | list[str] | *none* | Field projection -- keep only these keys per item. Reduces response size. |
Tools that accept these parameters are marked with "Supports pagination" below. Use `cursor_next(cursor_id)` to advance through pages.
---
## Instance Management
Tools for discovering, registering, and switching between Ghidra instances.
### `instances_list`
List all active Ghidra instances. Runs a quick discovery scan before returning results.
Returns a dict with an `instances` list containing port, URL, project, and file for each instance.
### `instances_use`
Set the current working instance. All subsequent tool calls default to this instance.
Uses lazy registration -- the instance is recorded immediately without a blocking HTTP call. If the instance is unreachable, the next actual tool call will fail with a clear error.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *required* | Port number of the instance to activate |
Returns confirmation with instance details.
### `instances_current`
Show which instance is currently active, including its port, URL, project, and file. Returns an error message with available instance ports if none is set.
### `instances_register`
Manually register an instance by port. Verifies the instance is responsive and checks API version compatibility before registering.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *required* | Port number |
| `url` | string | *auto* | URL override (defaults to `http://{GHIDRA_HOST}:{port}`) |
Returns confirmation or error message.
### `instances_unregister`
Remove an instance from the registry. If the unregistered instance was the current working instance, the current selection is cleared.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *required* | Port number to unregister |
### `instances_discover`
Force a full discovery scan across the configured port range (ports 18400-18600). Use this when you need to find instances on a different host. For normal use, `instances_list` already runs a quick scan.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `host` | string | *configured* | Host to scan |
### `program_info`
Get full program metadata from the current Ghidra instance: architecture, language ID, compiler spec, image base address, and total memory size.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *current* | Ghidra instance port |
---
## Functions
Tools for listing, decompiling, disassembling, and modifying functions. Supports pagination.
### `functions_list`
List functions with cursor-based pagination and server-side filtering. For large binaries, use `name_contains` or `name_regex` for server-side filtering before results reach the client.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name_contains` | string | *none* | Server-side substring filter (faster than grep for large binaries) |
| `name_regex` | string | *none* | Server-side regex filter on function name |
| `address` | string | *none* | Filter by exact function address (hex) |
Supports pagination.
### `functions_get`
Get detailed information about a single function: name, address, signature, size, stack depth, calling convention.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Function name (mutually exclusive with address) |
| `address` | string | *none* | Function address in hex |
| `port` | int | *current* | Ghidra instance port |
### `functions_decompile`
Decompile a function to C pseudocode. Output is split into lines for pagination -- use `grep` to search within the decompiled code.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Function name (mutually exclusive with address) |
| `address` | string | *none* | Function address in hex |
| `syntax_tree` | bool | `false` | Include the decompiler syntax tree (JSON) |
| `style` | string | `"normalize"` | Decompiler simplification style |
Supports pagination (over decompiled lines).
### `functions_disassemble`
Get assembly-level disassembly for a function. Output is split into instruction lines for pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Function name (mutually exclusive with address) |
| `address` | string | *none* | Function address in hex |
Supports pagination (over instruction lines).
### `functions_rename`
Rename a function. Identify it by current name or address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `old_name` | string | *none* | Current function name |
| `address` | string | *none* | Function address in hex |
| `new_name` | string | *required* | New name for the function |
| `port` | int | *current* | Ghidra instance port |
### `functions_set_signature`
Set the full prototype of a function, including return type, name, and parameter types.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Function name |
| `address` | string | *none* | Function address in hex |
| `signature` | string | *required* | Full signature (e.g., `"int foo(char* arg1, int arg2)"`) |
| `port` | int | *current* | Ghidra instance port |
### `functions_set_comment`
Set a decompiler-level comment on a function. Tries the function comment first, then falls back to a pre-comment if the address is not a function entry point.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address (preferably function entry point) |
| `comment` | string | `""` | Comment text. Empty string removes the comment. |
| `port` | int | *current* | Ghidra instance port |
### `functions_create`
Create a new function definition at the specified address. Ghidra will attempt to determine the function boundaries automatically.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `port` | int | *current* | Ghidra instance port |
### `functions_variables`
List local variables and parameters for a specific function. Supports pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Function address in hex |
Supports pagination.
---
## Data
Tools for working with defined data items and strings.
### `data_list`
List defined data items with filtering and cursor-based pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `addr` | string | *none* | Filter by address (hex) |
| `name` | string | *none* | Exact name match (case-sensitive) |
| `name_contains` | string | *none* | Substring name filter (case-insensitive) |
| `type` | string | *none* | Filter by data type (e.g., `"string"`, `"dword"`) |
Supports pagination.
### `data_list_strings`
List all defined strings in the binary. Use `filter` for server-side content matching, or `grep` for client-side regex.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `filter` | string | *none* | Server-side string content filter |
Supports pagination.
### `data_create`
Define a new data item at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `data_type` | string | *required* | Data type (e.g., `"string"`, `"dword"`, `"byte"`) |
| `size` | int | *none* | Size in bytes (optional) |
| `port` | int | *current* | Ghidra instance port |
### `data_rename`
Rename a data item at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `name` | string | *required* | New name |
| `port` | int | *current* | Ghidra instance port |
### `data_set_type`
Change the data type of an existing data item.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `data_type` | string | *required* | New data type (e.g., `"uint32_t"`, `"char[10]"`) |
| `port` | int | *current* | Ghidra instance port |
### `data_delete`
Remove a data definition at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `port` | int | *current* | Ghidra instance port |
---
## Structs
Tools for creating and modifying struct (composite) data types.
### `structs_list`
List all struct data types. Supports pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `category` | string | *none* | Filter by category path (e.g., `"/winapi"`) |
Supports pagination.
### `structs_get`
Get a struct with all its fields. If the struct has more than 10 fields, the field list is paginated. Use `fields` projection to reduce response size.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Struct name |
Supports pagination (over struct fields).
### `structs_create`
Create a new struct data type.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Name for the struct |
| `category` | string | *none* | Category path (e.g., `"/custom"`) |
| `description` | string | *none* | Description text |
| `port` | int | *current* | Ghidra instance port |
### `structs_add_field`
Add a field to an existing struct. If `offset` is omitted, the field is appended to the end of the struct.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `struct_name` | string | *required* | Name of the struct |
| `field_name` | string | *required* | Name for the new field |
| `field_type` | string | *required* | Data type (e.g., `"int"`, `"char"`, `"pointer"`) |
| `offset` | int | *none* | Byte offset within the struct |
| `comment` | string | *none* | Field comment |
| `port` | int | *current* | Ghidra instance port |
### `structs_update_field`
Modify an existing field in a struct. Identify the field by name or offset. At least one of `new_name`, `new_type`, or `new_comment` must be provided.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `struct_name` | string | *required* | Name of the struct |
| `field_name` | string | *none* | Current field name (or use `field_offset`) |
| `field_offset` | int | *none* | Field offset (or use `field_name`) |
| `new_name` | string | *none* | New name |
| `new_type` | string | *none* | New data type |
| `new_comment` | string | *none* | New comment |
| `port` | int | *current* | Ghidra instance port |
### `structs_delete`
Remove a struct data type definition.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Struct name to delete |
| `port` | int | *current* | Ghidra instance port |
---
## Symbols
Tools for working with the symbol table: labels, imports, and exports.
### `symbols_list`
List all symbols in the program. Supports pagination.
Supports pagination.
### `symbols_create`
Create a new label/symbol at the specified address. If a symbol already exists at that address, it is renamed.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Symbol name |
| `address` | string | *required* | Memory address in hex |
| `port` | int | *current* | Ghidra instance port |
### `symbols_rename`
Rename the primary symbol at an address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `new_name` | string | *required* | New name |
| `port` | int | *current* | Ghidra instance port |
### `symbols_delete`
Delete the primary symbol at an address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `port` | int | *current* | Ghidra instance port |
### `symbols_imports`
List imported symbols (external references). Supports pagination.
Supports pagination.
### `symbols_exports`
List exported symbols (entry points). Supports pagination.
Supports pagination.
---
## Analysis
Tools for triggering and inspecting Ghidra analysis results.
### `analysis_run`
Trigger Ghidra's auto-analysis on the current program.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *current* | Ghidra instance port |
| `analysis_options` | dict | *none* | Analysis options to enable/disable |
### `analysis_get_callgraph`
Generate a call graph starting from a function. Returns nodes and edges. Edges are paginated.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Starting function name |
| `address` | string | *none* | Starting function address |
| `max_depth` | int | `3` | Maximum call depth |
Supports pagination (over edges).
### `analysis_get_dataflow`
Trace data flow forward or backward from an address. Returns a list of steps showing how data propagates through the program.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Starting address in hex |
| `direction` | string | `"forward"` | `"forward"` or `"backward"` |
| `max_steps` | int | `50` | Maximum analysis steps |
Supports pagination (over steps).
### `xrefs_list`
Find cross-references to or from an address. At least one of `to_addr` or `from_addr` is required.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `to_addr` | string | *none* | Find references to this address |
| `from_addr` | string | *none* | Find references from this address |
| `type` | string | *none* | Filter by type: `"CALL"`, `"READ"`, `"WRITE"`, `"DATA"`, `"POINTER"` |
Supports pagination.
### `comments_get`
Get a comment at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `comment_type` | string | `"plate"` | Type: `"plate"`, `"pre"`, `"post"`, `"eol"`, `"repeatable"` |
| `port` | int | *current* | Ghidra instance port |
### `comments_set`
Set a comment at the specified address. Pass an empty string to remove the comment.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `comment` | string | `""` | Comment text |
| `comment_type` | string | `"plate"` | Type: `"plate"`, `"pre"`, `"post"`, `"eol"`, `"repeatable"` |
| `port` | int | *current* | Ghidra instance port |
---
## Memory
Direct memory access tools.
### `memory_read`
Read bytes from a memory address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `length` | int | `16` | Number of bytes to read |
| `format` | string | `"hex"` | Output format: `"hex"`, `"base64"`, or `"string"` |
| `port` | int | *current* | Ghidra instance port |
Returns the bytes in the requested format along with the actual byte count.
### `memory_write`
Write bytes to a memory address. Use with caution -- this modifies the program state.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `bytes_data` | string | *required* | Data to write |
| `format` | string | `"hex"` | Input format: `"hex"`, `"base64"`, or `"string"` |
| `port` | int | *current* | Ghidra instance port |
---
## Variables
Tools for querying and renaming variables.
### `variables_list`
List variables with optional global-only filtering. Supports pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `global_only` | bool | `false` | Return only global variables |
Supports pagination.
### `variables_rename`
Rename a variable within a function, and optionally change its data type.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `function_address` | string | *required* | Function address in hex |
| `variable_name` | string | *required* | Current variable name |
| `new_name` | string | *required* | New name |
| `new_type` | string | *none* | New data type (e.g., `"int"`, `"char*"`) |
| `port` | int | *current* | Ghidra instance port |
---
## Bookmarks
Tools for managing Ghidra bookmarks (annotations at addresses).
### `bookmarks_list`
List bookmarks with optional type and category filtering. Supports pagination.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `type` | string | *none* | Filter by type: `"Note"`, `"Warning"`, `"Error"`, `"Info"` |
| `category` | string | *none* | Filter by category |
Supports pagination.
### `bookmarks_create`
Create a bookmark at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `type` | string | `"Note"` | Bookmark type: `Note`, `Warning`, `Error`, `Info` |
| `category` | string | `""` | Category string for grouping |
| `comment` | string | `""` | Bookmark comment text |
| `port` | int | *current* | Ghidra instance port |
### `bookmarks_delete`
Delete all bookmarks at the specified address.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `address` | string | *required* | Memory address in hex |
| `port` | int | *current* | Ghidra instance port |
---
## Enums and Typedefs
Tools for managing enum and typedef data types.
### `enums_list`
List enum data types with their members. Supports pagination.
Supports pagination.
### `enums_create`
Create a new enum data type.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Enum name |
| `size` | int | `4` | Size in bytes |
| `port` | int | *current* | Ghidra instance port |
### `typedefs_list`
List typedef data types. Supports pagination.
Supports pagination.
### `typedefs_create`
Create a new typedef data type.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *required* | Typedef name |
| `base_type` | string | *required* | Base data type (e.g., `"int"`, `"uint32_t"`, `"char*"`) |
| `port` | int | *current* | Ghidra instance port |
---
## Namespaces
Tools for querying namespaces and class definitions.
### `namespaces_list`
List all non-global namespaces in the program. Supports pagination.
Supports pagination.
### `classes_list`
List class namespaces with qualified names. Supports pagination.
Supports pagination.
---
## Segments
### `segments_list`
List memory segments (`.text`, `.data`, `.bss`, etc.) with read/write/execute permissions, start address, and size.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | string | *none* | Filter by segment name (exact match) |
Supports pagination.
---
## Cursors
Tools for managing pagination state. Every paginated tool response includes a `cursor_id` in the pagination metadata when more pages are available.
### `cursor_next`
Fetch the next page of results for a cursor. Cursors expire after 5 minutes of inactivity.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cursor_id` | string | *required* | Cursor identifier from a previous paginated response |
Returns the next page of results with updated pagination info.
### `cursor_list`
List active cursors for the current session.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `all_sessions` | bool | `false` | Include cursors from all sessions |
### `cursor_delete`
Delete a specific cursor to free resources.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cursor_id` | string | *required* | Cursor identifier to delete |
### `cursor_delete_all`
Delete all cursors for the current session.
---
## Docker
Tools for managing Ghidra Docker containers. See the [Docker Usage](/reference/docker/) page for environment variables and firmware import details.
### `docker_auto_start`
The primary entry point for automatic container management. Checks all pooled ports for an existing instance analyzing the same binary. If none is found, allocates a port and starts a new container. Returns connection info immediately -- poll `docker_health` to check when the API is ready.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `binary_path` | string | *required* | Path to the binary file |
| `language` | string | *none* | Ghidra processor language ID (e.g., `"ARM:LE:32:v4t"`) |
| `base_address` | string | *none* | Base address for raw binaries (e.g., `"0x00000000"`) |
| `loader` | string | *none* | Loader type. Auto-set to `"BinaryLoader"` when language is specified. |
### `docker_start`
Start a container with explicit control over all parameters. Ports are auto-allocated from the pool (8192-8319).
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `binary_path` | string | *required* | Path to the binary file |
| `memory` | string | `"2G"` | Max JVM heap size |
| `name` | string | *auto* | Container name (auto-generated with session ID) |
| `language` | string | *none* | Ghidra processor language ID |
| `base_address` | string | *none* | Base address (hex) |
| `loader` | string | *none* | Loader type |
### `docker_stop`
Stop and optionally remove a container. Session-scoped: you can only stop containers started by your own MCP session.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name_or_id` | string | *required* | Container name or ID |
| `remove` | bool | `true` | Also remove the container |
### `docker_health`
Check if a container's HTTP API is responding. Tries `/health` first, then falls back to the root endpoint for older plugin versions.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | int | *current* | API port to check |
| `timeout` | float | `5.0` | Request timeout in seconds |
### `docker_logs`
Get stdout/stderr from a container. Useful for monitoring analysis progress.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `name_or_id` | string | *required* | Container name or ID |
| `tail` | int | `100` | Number of lines to show |
| `follow` | bool | `false` | Follow log output (not recommended for MCP) |
### `docker_status`
List all MCGhidra containers, Docker images, port pool allocation status, and whether Docker/Compose are available.
### `docker_build`
Build the MCGhidra Docker image from source.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `tag` | string | `"latest"` | Image tag |
| `no_cache` | bool | `false` | Build without Docker cache |
| `project_dir` | string | *auto* | Path to MCGhidra project root |
### `docker_cleanup`
Remove orphaned containers and stale port lock files. By default, only cleans containers from the current session for safety.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `session_only` | bool | `true` | Only clean this session's containers |
| `max_age_hours` | float | `24.0` | Max age for orphaned containers |
| `dry_run` | bool | `false` | Report what would be cleaned without acting |
### `docker_session_info`
Show containers and allocated ports for the current MCP session.

View File

@ -1,203 +0,0 @@
---
title: MCP Resources
description: Reference for MCGhidra's read-only MCP resource URIs
---
MCGhidra registers 19 MCP resources that provide read-only access to Ghidra data. Resources are a good fit for quick enumeration -- they return data without requiring tool calls and work well for populating context at the start of a conversation.
## Resources vs Tools
Resources and tools serve different purposes:
- **Resources** return a capped snapshot of data. They have no pagination controls, no filtering, and a fixed maximum result size. Use them for a quick overview: "what functions exist in this binary?" or "what strings are defined?"
- **Tools** support pagination, grep filtering, field projection, and mutation operations. Use them when you need to page through large result sets, search for specific items, or modify the program.
If a resource hits its cap, the response includes a `_hint` field suggesting which tool to use for full pagination.
## Result Caps
Each resource type has a configurable maximum number of items it will return. These defaults are set in `MCGhidraConfig.resource_caps`:
| Resource Type | Default Cap |
|---------------|-------------|
| functions | 1000 |
| strings | 500 |
| data | 1000 |
| structs | 500 |
| xrefs | 500 |
| symbols | 1000 |
| segments | 500 |
| variables | 1000 |
| namespaces | 500 |
| classes | 500 |
| bookmarks | 1000 |
| enums | 500 |
| typedefs | 500 |
---
## Instance Resources
### `ghidra://instances`
List all active Ghidra instances. Runs a quick discovery scan before returning.
Returns: `instances` array (port, project, file), `count`, and `current_port`.
### `ghidra://instance/{port}`
Get detailed information about a specific Ghidra instance, including program metadata from the plugin's root endpoint.
**URI parameter:** `port` -- the instance port number.
### `ghidra://instance/{port}/summary`
Program overview with aggregate statistics. Fetches function count and string count in addition to basic program metadata (name, language, processor, format).
**URI parameter:** `port` -- the instance port number.
### `ghidra://instance/{port}/program`
Program metadata: architecture, language ID, compiler spec, image base address, and memory size. This is the same data returned by the REST API's `GET /program` endpoint.
**URI parameter:** `port` -- the instance port number.
---
## Function Resources
### `ghidra://instance/{port}/functions`
List functions in the program. Capped at 1000 items.
Returns: `functions` array, `count`, and `capped_at` (non-null if the cap was reached).
If capped, use the `functions_list()` tool for full pagination.
### `ghidra://instance/{port}/function/decompile/address/{address}`
Decompile a function by its address. Returns the C pseudocode as a plain text string.
**URI parameters:** `port`, `address` (hex, e.g., `0x401000`).
### `ghidra://instance/{port}/function/decompile/name/{name}`
Decompile a function by name. Returns the C pseudocode as a plain text string.
**URI parameters:** `port`, `name` (function name, e.g., `main`).
---
## Data Resources
### `ghidra://instance/{port}/strings`
List defined strings in the binary. Capped at 500 items.
Returns: `strings` array, `count`, and `capped_at`.
If capped, use `data_list_strings()` for full pagination.
### `ghidra://instance/{port}/data`
List defined data items. Capped at 1000 items.
Returns: `data` array, `count`, and `capped_at`.
If capped, use `data_list()` for full pagination.
### `ghidra://instance/{port}/structs`
List struct data types. Capped at 500 items.
Returns: `structs` array, `count`, and `capped_at`.
If capped, use `structs_list()` for full pagination.
---
## Cross-Reference Resources
### `ghidra://instance/{port}/xrefs/to/{address}`
Get all cross-references pointing to the specified address. Capped at 500 items.
**URI parameters:** `port`, `address` (hex).
Returns: `address`, `xrefs_to` array, `count`, and `capped_at`.
If capped, use `xrefs_list(to_addr=...)` for full pagination.
### `ghidra://instance/{port}/xrefs/from/{address}`
Get all cross-references originating from the specified address. Capped at 500 items.
**URI parameters:** `port`, `address` (hex).
Returns: `address`, `xrefs_from` array, `count`, and `capped_at`.
If capped, use `xrefs_list(from_addr=...)` for full pagination.
---
## Symbol Resources
### `ghidra://instance/{port}/symbols`
List all symbols in the program. Capped at 1000 items.
Returns: `symbols` array, `count`, and `capped_at`.
If capped, use `symbols_list()` for full pagination.
### `ghidra://instance/{port}/symbols/imports`
List imported symbols (external references). Capped at 1000 items.
Returns: `imports` array, `count`, and `capped_at`.
If capped, use `symbols_imports()` for full pagination.
### `ghidra://instance/{port}/symbols/exports`
List exported symbols (entry points). Capped at 1000 items.
Returns: `exports` array, `count`, and `capped_at`.
If capped, use `symbols_exports()` for full pagination.
---
## Other Resources
### `ghidra://instance/{port}/segments`
List memory segments with names, address ranges, sizes, and permissions. Capped at 500 items.
Returns: `segments` array, `count`, and `capped_at`.
If capped, use `segments_list()` for full pagination.
### `ghidra://instance/{port}/namespaces`
List all non-global namespaces. Capped at 500 items.
Returns: `namespaces` array, `count`, and `capped_at`.
If capped, use `namespaces_list()` for full pagination.
### `ghidra://instance/{port}/classes`
List class namespaces with qualified names. Capped at 500 items.
Returns: `classes` array, `count`, and `capped_at`.
If capped, use `classes_list()` for full pagination.
### `ghidra://instance/{port}/variables`
List variables. Capped at 1000 items.
Returns: `variables` array, `count`, and `capped_at`.
If capped, use `variables_list()` for full pagination.

View File

@ -1,423 +0,0 @@
---
title: REST API
description: Reference for the Ghidra plugin's HATEOAS HTTP API
---
The Ghidra plugin runs an HTTP server inside the JVM and exposes a HATEOAS REST API. Every response includes hypermedia links (`_links`) to related resources, so clients can discover the API by following links rather than hardcoding paths.
The MCP server wraps this API as MCP tools. You generally do not need to call the REST API directly, but understanding it helps when debugging or building custom integrations.
## General Concepts
### Request Format
Standard HTTP verbs: `GET` to read, `POST` to create, `PATCH` to modify, `PUT` to replace, `DELETE` to remove. Request bodies use JSON (`Content-Type: application/json`). Include an `X-Request-ID` header for correlation if needed.
### Response Envelope
Every response follows this structure:
```json
{
"id": "req-123",
"instance": "http://localhost:8192",
"success": true,
"result": { ... },
"_links": {
"self": { "href": "/path/to/resource" },
"related": { "href": "/path/to/related" }
}
}
```
- `id` -- Correlation identifier from `X-Request-ID`, or a generated value.
- `instance` -- URL of the plugin instance that handled the request.
- `result` -- The payload. A single object for detail endpoints, an array for list endpoints.
- `_links` -- HATEOAS links to related resources and actions.
### Error Responses
Errors use standard HTTP status codes and include a structured error object:
```json
{
"id": "req-456",
"instance": "http://localhost:8192",
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "No function at address 0x999999"
}
}
```
Common status codes: `200` OK, `201` Created, `400` Bad Request, `404` Not Found, `500` Internal Server Error.
### Pagination
List endpoints accept `offset` and `limit` query parameters. Responses include `size` (total count), `offset`, `limit`, and `_links` with `next`/`prev` when applicable.
```
GET /functions?offset=50&limit=50
```
### Addressing and Search
Resources can be accessed by hex address or searched by name:
- By address: `GET /functions/0x401000`
- By exact name: `GET /functions?name=main`
- By substring: `GET /functions?name_contains=init`
- By regex: `GET /functions?name_matches_regex=^FUN_`
---
## Meta Endpoints
### `GET /plugin-version`
Returns the plugin build version and API version number. The MCP server uses this for compatibility checks.
```json
{
"result": {
"plugin_version": "v2.0.0",
"api_version": 2
}
}
```
### `GET /info`
Returns details about the current plugin instance: loaded file, architecture, processor, address size, project name, and server port.
```json
{
"result": {
"file": "example.exe",
"architecture": "x86:LE:64:default",
"processor": "x86",
"addressSize": 64,
"project": "MyProject",
"serverPort": 8192,
"instanceCount": 1
}
}
```
### `GET /instances`
Lists all active plugin instances (one per open program in the Ghidra project). Each entry includes port, type, project, file, and links to connect.
### `GET /program`
Returns program metadata: language ID, compiler spec, image base address, memory size, and analysis status.
```json
{
"result": {
"name": "mybinary.exe",
"languageId": "x86:LE:64:default",
"compilerSpecId": "gcc",
"imageBase": "0x400000",
"memorySize": 1048576,
"analysisComplete": true
}
}
```
---
## Functions
### `GET /functions`
List functions. Supports pagination and search parameters (`name`, `name_contains`, `name_matches_regex`, `addr`).
```json
{
"result": [
{ "name": "main", "address": "0x401000" },
{ "name": "init_peripherals", "address": "0x08001cf0" }
],
"size": 150,
"offset": 0,
"limit": 50
}
```
### `POST /functions`
Create a function at an address. Body: `{ "address": "0x401000" }`.
### `GET /functions/{address}`
Get function details: name, signature, size, stack depth, calling convention, varargs status.
```json
{
"result": {
"name": "process_data",
"address": "0x4010a0",
"signature": "int process_data(char * data, int size)",
"size": 128,
"calling_convention": "__stdcall"
}
}
```
### `PATCH /functions/{address}`
Modify a function. Payload can include `name`, `signature`, and `comment`.
```json
{ "name": "calculate_checksum", "signature": "uint32_t calculate_checksum(uint8_t* buffer, size_t length)" }
```
### `DELETE /functions/{address}`
Delete the function definition at the specified address.
### `GET /functions/{address}/decompile`
Get decompiled C pseudocode. Optional query parameters:
| Parameter | Description |
|-----------|-------------|
| `syntax_tree` | `true` to include the syntax tree as JSON |
| `style` | Decompiler simplification style (e.g., `normalize`) |
| `timeout` | Decompilation timeout in seconds |
```json
{
"result": {
"address": "0x4010a0",
"ccode": "int process_data(char *param_1, int param_2)\n{\n ...\n}\n"
}
}
```
### `GET /functions/{address}/disassembly`
Get assembly listing. Supports pagination (`offset`, `limit`).
```json
{
"result": [
{ "address": "0x4010a0", "mnemonic": "PUSH", "operands": "RBP", "bytes": "55" },
{ "address": "0x4010a1", "mnemonic": "MOV", "operands": "RBP, RSP", "bytes": "4889E5" }
]
}
```
### `GET /functions/{address}/variables`
List local variables for a function. Supports name search.
### `PATCH /functions/{address}/variables/{variable_name}`
Modify a local variable. Payload: `{ "name": "new_name", "type": "int" }`.
---
## Data
### `GET /data`
List defined data items. Supports search (`name`, `name_contains`, `addr`, `type`) and pagination.
### `POST /data`
Define data at an address. Body: `{ "address": "0x402000", "type": "dword" }`.
### `GET /data/{address}`
Get data item details (type, size, value representation).
### `PATCH /data/{address}`
Modify a data item: change `name`, `type`, or `comment`.
### `DELETE /data/{address}`
Undefine the data item at the specified address.
### `GET /strings`
List defined strings. Supports pagination and a `filter` parameter for substring matching.
```json
{
"result": [
{ "address": "0x00401234", "value": "Hello, world!", "length": 14, "type": "string" },
{ "address": "0x00401250", "value": "Error: could not open file", "length": 26, "type": "string" }
]
}
```
---
## Structs
### `GET /structs`
List struct data types. Supports pagination and `category` filtering.
### `GET /structs?name={name}`
Get detailed struct information including all fields with offsets, types, and comments.
```json
{
"result": {
"name": "MyStruct",
"size": 16,
"category": "/custom",
"fields": [
{ "name": "id", "offset": 0, "length": 4, "type": "int", "comment": "Unique identifier" },
{ "name": "flags", "offset": 4, "length": 4, "type": "dword", "comment": "" }
]
}
}
```
### `POST /structs/create`
Create a struct. Body: `{ "name": "NetworkPacket", "category": "/network" }`.
### `POST /structs/addfield`
Add a field. Body: `{ "struct": "NetworkPacket", "fieldName": "header", "fieldType": "dword" }`.
### `POST /structs/updatefield`
Update a field. Identify by `fieldName` or `fieldOffset`, then provide `newName`, `newType`, and/or `newComment`.
### `POST /structs/delete`
Delete a struct. Body: `{ "name": "NetworkPacket" }`.
---
## Symbols
### `GET /symbols`
List all symbols. Supports search and pagination. Can filter by `type` (`function`, `data`, `label`).
### `POST /symbols`
Create or rename a symbol. Body: `{ "address": "0x401000", "name": "my_label" }`.
### `PATCH /symbols/{address}`
Modify a symbol (rename, change namespace, set as primary).
### `DELETE /symbols/{address}`
Remove the symbol at the specified address.
---
## Memory
### `GET /memory/{address}`
Read bytes from memory.
| Parameter | Description |
|-----------|-------------|
| `length` | Number of bytes (required, server-imposed max) |
| `format` | `hex`, `base64`, or `string` (default: `hex`) |
```json
{
"result": {
"address": "0x402000",
"length": 16,
"format": "hex",
"bytes": "48656C6C6F20576F726C642100000000"
}
}
```
### `PATCH /memory/{address}`
Write bytes. Body: `{ "bytes": "DEADBEEF", "format": "hex" }`. Use with caution.
---
## Segments
### `GET /segments`
List memory segments (`.text`, `.data`, `.bss`, etc.) with address ranges, sizes, and R/W/X permissions.
### `GET /segments/{name}`
Get details for a specific segment.
---
## Cross-References
### `GET /xrefs`
Find cross-references. At least one query parameter is required:
| Parameter | Description |
|-----------|-------------|
| `to_addr` | References pointing to this address |
| `from_addr` | References originating from this address |
| `type` | Filter: `CALL`, `READ`, `WRITE`, `DATA`, `POINTER` |
Supports pagination.
---
## Analysis
### `GET /analysis`
Get analysis status and list of available analyzers.
```json
{
"result": {
"program": "mybinary.exe",
"analysis_enabled": true,
"available_analyzers": [
"Function Start Analyzer",
"Reference Analyzer",
"Decompiler Parameter ID"
]
}
}
```
### `POST /analysis`
Trigger re-analysis of the program.
### `GET /analysis/callgraph`
Generate a call graph.
| Parameter | Default | Description |
|-----------|---------|-------------|
| `function` | *entry point* | Starting function name |
| `max_depth` | `3` | Maximum call depth |
Returns `nodes` (functions) and `edges` (calls between them with call-site addresses).
### `GET /analysis/dataflow`
Trace data flow from an address.
| Parameter | Default | Description |
|-----------|---------|-------------|
| `address` | *required* | Starting address |
| `direction` | `forward` | `forward` or `backward` |
| `max_steps` | `50` | Maximum analysis steps |
Returns a list of `steps`, each with an address, instruction, and description.

View File

@ -1 +0,0 @@
/// <reference path="../.astro/types.d.ts" />

View File

@ -1,62 +0,0 @@
/* MCGhidra reverse engineering tooling aesthetic
* Warm amber/rust accent (#c85533), dark grays
*/
/* Light mode */
:root {
--sl-color-accent-low: #f8e4db;
--sl-color-accent: #b5452a;
--sl-color-accent-high: #6e2915;
--sl-color-white: #1a1614;
--sl-color-gray-1: #3a3230;
--sl-color-gray-2: #5c524e;
--sl-color-gray-3: #8a7e78;
--sl-color-gray-4: #b0a49e;
--sl-color-gray-5: #d0c8c4;
--sl-color-gray-6: #eae4e0;
--sl-color-gray-7: #f6f2f0;
--sl-color-black: #faf8f6;
--sl-font: 'Segoe UI', system-ui, -apple-system, sans-serif;
--sl-font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
}
/* Dark mode */
:root[data-theme='dark'] {
--sl-color-accent-low: #3a1a0c;
--sl-color-accent: #e07040;
--sl-color-accent-high: #f5c4aa;
--sl-color-white: #f6f2f0;
--sl-color-gray-1: #d0c8c4;
--sl-color-gray-2: #b0a49e;
--sl-color-gray-3: #8a7e78;
--sl-color-gray-4: #5c524e;
--sl-color-gray-5: #3a3230;
--sl-color-gray-6: #261e1a;
--sl-color-gray-7: #1a1614;
--sl-color-black: #100c0a;
}
/* Inline code background */
:root {
--sl-color-bg-inline-code: #eae4e0;
}
:root[data-theme='dark'] {
--sl-color-bg-inline-code: #261e1a;
}
/* Tighter sidebar spacing */
nav.sidebar .top-level > li + li {
margin-top: 0.25rem;
}
/* Code blocks and disassembly tables */
table {
font-size: 0.9rem;
}
th {
background: var(--sl-color-gray-6);
font-weight: 600;
}

View File

@ -1,3 +0,0 @@
{
"extends": "astro/tsconfigs/strict"
}

20
pom.xml
View File

@ -4,11 +4,11 @@
<modelVersion>4.0.0</modelVersion>
<groupId>eu.starsong.ghidra</groupId>
<artifactId>MCGhidra</artifactId>
<artifactId>GhydraMCP</artifactId>
<packaging>jar</packaging>
<version>dev</version>
<name>MCGhidra</name>
<url>https://github.com/starsong-consulting/MCGhidra</url>
<name>GhydraMCP</name>
<url>https://github.com/starsong-consulting/GhydraMCP</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -25,7 +25,7 @@
<maven.install.skip>true</maven.install.skip>
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>
<revision>dev</revision>
<inner.zip.filename>MCGhidra-${git.commit.id.describe}-${maven.build.timestamp}.zip</inner.zip.filename>
<inner.zip.filename>GhydraMCP-${git.commit.id.describe}-${maven.build.timestamp}.zip</inner.zip.filename>
</properties>
<dependencies>
@ -153,16 +153,16 @@
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Implementation-Title>MCGhidra</Implementation-Title>
<Implementation-Title>GhydraMCP</Implementation-Title>
<Implementation-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Implementation-Version>
<Plugin-Class>eu.starsong.ghidra.MCGhidra</Plugin-Class>
<Plugin-Name>MCGhidra</Plugin-Name>
<Plugin-Class>eu.starsong.ghidra.GhydraMCP</Plugin-Class>
<Plugin-Name>GhydraMCP</Plugin-Name>
<Plugin-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Plugin-Version>
<Plugin-Author>LaurieWired, Teal Bauer</Plugin-Author>
<Plugin-Description>Expose multiple Ghidra tools to MCP servers with variable management</Plugin-Description>
</manifestEntries>
</archive>
<finalName>MCGhidra</finalName>
<finalName>GhydraMCP</finalName>
<excludes>
<exclude>**/App.class</exclude>
</excludes>
@ -187,7 +187,7 @@
<descriptors>
<descriptor>src/assembly/ghidra-extension.xml</descriptor>
</descriptors>
<finalName>MCGhidra-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
<finalName>GhydraMCP-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
@ -203,7 +203,7 @@
<descriptors>
<descriptor>src/assembly/complete-package.xml</descriptor>
</descriptors>
<finalName>MCGhidra-Complete-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
<finalName>GhydraMCP-Complete-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>

View File

@ -1,7 +1,7 @@
[project]
name = "mcghidra"
version = "2026.3.7"
description = "Reverse engineering bridge: multi-instance Ghidra plugin with HATEOAS REST API and MCP server for decompilation, analysis & binary manipulation"
name = "ghydramcp"
version = "2025.12.3"
description = "AI-assisted reverse engineering bridge: a multi-instance Ghidra plugin exposed via a HATEOAS REST API plus an MCP Python bridge for decompilation, analysis & binary manipulation"
readme = "README.md"
requires-python = ">=3.11"
authors = [
@ -15,19 +15,14 @@ dependencies = [
]
[project.scripts]
mcghidra = "mcghidra:main"
[project.urls]
Documentation = "https://mcghidra.warehack.ing"
Repository = "https://git.supported.systems/MCP/mcghidra"
Issues = "https://git.supported.systems/MCP/mcghidra/issues"
ghydramcp = "ghydramcp:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcghidra"]
packages = ["src/ghydramcp"]
[tool.hatch.build]
sources = ["src"]

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Test runner for MCGhidra tests.
Test runner for GhydraMCP tests.
This script runs both the HTTP API tests and the MCP bridge tests.
"""
import os
@ -21,10 +21,10 @@ def run_http_api_tests():
# Import and run the tests
try:
from test_http_api import MCGhidraHttpApiTests
from test_http_api import GhydraMCPHttpApiTests
# Create a test suite with all tests from MCGhidraHttpApiTests
suite = unittest.TestLoader().loadTestsFromTestCase(MCGhidraHttpApiTests)
# Create a test suite with all tests from GhydraMCPHttpApiTests
suite = unittest.TestLoader().loadTestsFromTestCase(GhydraMCPHttpApiTests)
# Run the tests
result = unittest.TextTestRunner(verbosity=2).run(suite)
@ -118,7 +118,7 @@ def run_comment_tests():
def run_all_tests():
"""Run all tests"""
print_header("MCGhidra Test Suite")
print_header("GhydraMCP Test Suite")
# Run test suites
http_api_success = run_http_api_tests()

View File

@ -11,23 +11,23 @@
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<!-- Copy extension files to MCGhidra/ directory -->
<!-- Copy extension files to GhydraMCP/ directory -->
<fileSet>
<directory>src/main/resources</directory>
<includes>
<include>extension.properties</include>
<include>Module.manifest</include>
</includes>
<outputDirectory>MCGhidra</outputDirectory>
<outputDirectory>GhydraMCP</outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<!-- Include the main project JAR as MCGhidra.jar -->
<!-- Include the main project JAR as GhydraMCP.jar -->
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>MCGhidra/lib</outputDirectory>
<outputFileNameMapping>MCGhidra.jar</outputFileNameMapping>
<outputDirectory>GhydraMCP/lib</outputDirectory>
<outputFileNameMapping>GhydraMCP.jar</outputFileNameMapping>
<unpack>false</unpack>
</dependencySet>
</dependencySets>

15
src/ghydramcp/__init__.py Normal file
View File

@ -0,0 +1,15 @@
"""GhydraMCP - AI-assisted reverse engineering bridge for Ghidra.
A multi-instance Ghidra plugin exposed via HATEOAS REST API plus an MCP
Python bridge for decompilation, analysis & binary manipulation.
"""
try:
from importlib.metadata import version
__version__ = version("ghydramcp")
except Exception:
__version__ = "2025.12.1"
from .server import create_server, main
__all__ = ["create_server", "main", "__version__"]

View File

@ -0,0 +1,9 @@
"""GhydraMCP package entry point.
Allows running with: python -m ghydramcp
"""
from .server import main
if __name__ == "__main__":
main()

View File

@ -1,4 +1,4 @@
"""Configuration management for MCGhidra.
"""Configuration management for GhydraMCP.
Handles environment variables, default settings, and runtime configuration.
"""
@ -14,18 +14,18 @@ class DockerConfig:
"""Docker-specific configuration."""
# Docker image settings
image_name: str = "mcghidra"
image_tag: str = field(default_factory=lambda: os.environ.get("MCGHIDRAMCP_VERSION", "latest"))
image_name: str = "ghydramcp"
image_tag: str = field(default_factory=lambda: os.environ.get("GHYDRAMCP_VERSION", "latest"))
# Default container settings
default_port: int = field(default_factory=lambda: int(os.environ.get("MCGHIDRA_PORT", "8192")))
default_memory: str = field(default_factory=lambda: os.environ.get("MCGHIDRA_MAXMEM", "2G"))
default_port: int = field(default_factory=lambda: int(os.environ.get("GHYDRA_PORT", "8192")))
default_memory: str = field(default_factory=lambda: os.environ.get("GHYDRA_MAXMEM", "2G"))
# Project directory (for building)
project_dir: Optional[Path] = None
# Auto-start settings
auto_start_enabled: bool = field(default_factory=lambda: os.environ.get("MCGHIDRA_DOCKER_AUTO", "false").lower() == "true")
auto_start_enabled: bool = field(default_factory=lambda: os.environ.get("GHYDRA_DOCKER_AUTO", "false").lower() == "true")
auto_start_wait: bool = True
auto_start_timeout: float = 300.0
@ -49,8 +49,8 @@ def set_docker_config(config: DockerConfig) -> None:
@dataclass
class MCGhidraConfig:
"""Configuration for MCGhidra server."""
class GhydraConfig:
"""Configuration for GhydraMCP server."""
# Ghidra connection settings
ghidra_host: str = field(default_factory=lambda: os.environ.get("GHIDRA_HOST", "localhost"))
@ -81,12 +81,12 @@ class MCGhidraConfig:
# Feedback collection
feedback_enabled: bool = field(
default_factory=lambda: os.environ.get("MCGHIDRA_FEEDBACK", "true").lower() == "true"
default_factory=lambda: os.environ.get("GHYDRA_FEEDBACK", "true").lower() == "true"
)
feedback_db_path: str = field(
default_factory=lambda: os.environ.get(
"MCGHIDRA_FEEDBACK_DB",
str(Path.home() / ".mcghidra" / "feedback.db"),
"GHYDRA_FEEDBACK_DB",
str(Path.home() / ".ghydramcp" / "feedback.db"),
)
)
@ -114,18 +114,18 @@ class MCGhidraConfig:
# Global configuration instance (can be replaced for testing)
_config: Optional[MCGhidraConfig] = None
_config: Optional[GhydraConfig] = None
def get_config() -> MCGhidraConfig:
def get_config() -> GhydraConfig:
"""Get the global configuration instance."""
global _config
if _config is None:
_config = MCGhidraConfig()
_config = GhydraConfig()
return _config
def set_config(config: MCGhidraConfig) -> None:
def set_config(config: GhydraConfig) -> None:
"""Set the global configuration instance."""
global _config
_config = config

View File

@ -1,4 +1,4 @@
"""Core infrastructure for MCGhidra.
"""Core infrastructure for GhydraMCP.
Contains HTTP client, pagination, progress reporting, and logging utilities.
"""

View File

@ -1,4 +1,4 @@
"""Field projection and response size guard for MCGhidra.
"""Field projection and response size guard for GhydraMCP.
Provides jq-style field projection, grep filtering, and token budget
enforcement to prevent oversized MCP tool results.
@ -7,7 +7,7 @@ enforcement to prevent oversized MCP tool results.
import json
import re
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from ..config import get_config
@ -15,7 +15,7 @@ from ..config import get_config
TOKEN_ESTIMATION_RATIO = 4.0
def project_fields(items: List[Any], fields: List[str]) -> List[Any]:
def project_fields(items: list, fields: list[str]) -> list:
"""Select only specified keys from each item (jq-style projection).
Works on dicts and strings. For dicts, returns only the requested
@ -42,7 +42,7 @@ def project_fields(items: List[Any], fields: List[str]) -> List[Any]:
return projected
def apply_grep(items: List[Any], pattern: str, ignorecase: bool = True) -> List[Any]:
def apply_grep(items: list, pattern: str, ignorecase: bool = True) -> list:
"""Filter items by regex pattern across all string values.
Searches all string-coercible values in each item. For dicts,
@ -90,33 +90,13 @@ def _matches(item: Any, pattern: re.Pattern, depth: int = 0) -> bool:
def _estimate_tokens(data: Any) -> int:
"""Estimate token count from serialized JSON size.
Uses a simple heuristic: ~4 characters per token on average.
This matches the TOKEN_ESTIMATION_RATIO constant.
Args:
data: Any JSON-serializable data structure
Returns:
Estimated token count
"""
"""Estimate token count from serialized JSON size."""
text = json.dumps(data, default=str)
return int(len(text) / TOKEN_ESTIMATION_RATIO)
def _extract_available_fields(items: List[Any]) -> List[str]:
"""Extract the set of field names from the first few dict items.
Samples up to 5 items to discover available keys, useful for
suggesting field projections to reduce response size.
Args:
items: List of items (only dicts are examined)
Returns:
Sorted list of unique field names (excludes internal _links)
"""
def _extract_available_fields(items: list) -> list[str]:
"""Extract the set of field names from the first few dict items."""
fields = set()
for item in items[:5]:
if isinstance(item, dict):
@ -127,11 +107,11 @@ def _extract_available_fields(items: List[Any]) -> List[str]:
def estimate_and_guard(
data: List[Any],
data: list,
tool_name: str,
budget: Optional[int] = None,
query_hints: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
) -> Dict[str, Any]:
"""Check if data exceeds token budget; return guard response if so.
If data fits within budget, returns None (caller should proceed
@ -184,17 +164,7 @@ def estimate_and_guard(
def _format_tokens(n: int) -> str:
"""Format token count for human-readable display.
Large numbers are abbreviated with 'k' suffix for readability
in error messages and hints.
Args:
n: Token count
Returns:
Formatted string (e.g., 45000 -> '45k', 500 -> '500')
"""
"""Format token count for display (e.g. 45000 -> '45k')."""
if n >= 1000:
return "%dk" % (n // 1000)
return str(n)
@ -202,7 +172,7 @@ def _format_tokens(n: int) -> str:
def _build_hints(
tool_name: str,
available_fields: List[str],
available_fields: list[str],
query_hints: Optional[Dict[str, Any]] = None,
) -> str:
"""Build actionable hint text for the guard message."""

View File

@ -1,7 +1,7 @@
"""HTTP client for Ghidra REST API communication.
Provides safe request methods with error handling, HATEOAS compliance,
and response simplification for MCP tool consumption.
and response simplification for AI agent consumption.
"""
import time
@ -244,7 +244,6 @@ def safe_post(
text_payload = None
if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None)
json_payload = data
else:
@ -266,11 +265,7 @@ def safe_put(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
Returns:
Response dict
"""
if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None)
else:
headers = None
headers = data.pop("headers", None) if isinstance(data, dict) else None
return _make_request("PUT", port, endpoint, json_data=data, headers=headers)
@ -285,11 +280,7 @@ def safe_patch(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]
Returns:
Response dict
"""
if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None)
else:
headers = None
headers = data.pop("headers", None) if isinstance(data, dict) else None
return _make_request("PATCH", port, endpoint, json_data=data, headers=headers)
@ -307,7 +298,7 @@ def safe_delete(port: int, endpoint: str) -> Dict[str, Any]:
def simplify_response(response: Dict[str, Any]) -> Dict[str, Any]:
"""Simplify HATEOAS response for MCP tool consumption.
"""Simplify HATEOAS response for AI agent consumption.
- Removes _links from result entries
- Flattens nested structures

View File

@ -8,10 +8,10 @@ import logging
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from fastmcp import Context
from mcp.server.fastmcp import Context
# Standard Python logger as fallback
logger = logging.getLogger("mcghidra")
logger = logging.getLogger("ghydramcp")
async def log_debug(ctx: Optional["Context"], message: str) -> None:
@ -75,7 +75,7 @@ async def log_error(ctx: Optional["Context"], message: str) -> None:
def configure_logging(level: int = logging.INFO) -> None:
"""Configure the standard logger for MCGhidra.
"""Configure the standard logger for GhydraMCP.
Args:
level: Logging level (default: INFO)

View File

@ -186,8 +186,8 @@ class CursorManager:
item for item in data if self._matches_grep(item, pattern)
]
# Create query hash (SHA-256 for consistency with cursor ID generation)
query_hash = hashlib.sha256(
# Create query hash
query_hash = hashlib.md5(
json.dumps(query_params, sort_keys=True, default=str).encode()
).hexdigest()[:12]

View File

@ -1,11 +1,11 @@
"""MCP Mixins for MCGhidra.
"""MCP Mixins for GhydraMCP.
Domain-specific mixins that organize tools, resources, and prompts by functionality.
Uses FastMCP's contrib.mcp_mixin pattern for clean modular organization.
"""
from .analysis import AnalysisMixin
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
from .bookmarks import BookmarksMixin
from .cursors import CursorsMixin
from .data import DataMixin
@ -22,7 +22,7 @@ from .variables import VariablesMixin
from .xrefs import XrefsMixin
__all__ = [
"MCGhidraMixinBase",
"GhydraMixinBase",
"InstancesMixin",
"FunctionsMixin",
"DataMixin",

View File

@ -1,4 +1,4 @@
"""Analysis mixin for MCGhidra.
"""Analysis mixin for GhydraMCP.
Provides tools for program analysis operations.
"""
@ -9,11 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
from ..config import get_config
from ..core.logging import logger
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class AnalysisMixin(MCGhidraMixinBase):
class AnalysisMixin(GhydraMixinBase):
"""Mixin for analysis operations.
Provides tools for:
@ -242,10 +241,41 @@ class AnalysisMixin(MCGhidraMixinBase):
return paginated
# NOTE: ui_get_current_address and ui_get_current_function were removed
# because they require Ghidra GUI context which is never available in
# headless MCP mode. Use functions_get(address=...) or data_list(addr=...)
# with explicit addresses instead.
@mcp_tool()
def ui_get_current_address(self, port: Optional[int] = None) -> Dict[str, Any]:
"""Get the address currently selected in Ghidra's UI.
Args:
port: Ghidra instance port (optional)
Returns:
Current address information
"""
try:
port = self.get_instance_port(port)
except ValueError as e:
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
response = self.safe_get(port, "address")
return self.simplify_response(response)
@mcp_tool()
def ui_get_current_function(self, port: Optional[int] = None) -> Dict[str, Any]:
"""Get the function currently selected in Ghidra's UI.
Args:
port: Ghidra instance port (optional)
Returns:
Current function information
"""
try:
port = self.get_instance_port(port)
except ValueError as e:
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
response = self.safe_get(port, "function")
return self.simplify_response(response)
@mcp_tool()
def comments_get(
@ -350,18 +380,13 @@ class AnalysisMixin(MCGhidraMixinBase):
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
# Try setting as function comment first
try:
payload = {"comment": comment}
response = self.safe_patch(port, f"functions/{address}", payload)
if response.get("success", False):
return self.simplify_response(response)
# Log why function comment failed before falling back
error = response.get("error", {})
logger.debug(
"Function comment at %s failed (%s), falling back to pre-comment",
address,
error.get("code", "UNKNOWN"),
)
except Exception:
pass
# Fallback to pre-comment
return self.comments_set(

View File

@ -1,4 +1,4 @@
"""Base mixin class for MCGhidra domain mixins.
"""Base mixin class for GhydraMCP domain mixins.
Provides shared state and utilities for all domain mixins.
"""
@ -23,8 +23,8 @@ from ..core.logging import log_debug, log_error, log_info, log_warning
from ..core.pagination import paginate_response
class MCGhidraMixinBase(MCPMixin):
"""Base class for MCGhidra domain mixins.
class GhydraMixinBase(MCPMixin):
"""Base class for GhydraMCP domain mixins.
Provides shared instance state and common utilities.
All domain mixins should inherit from this class.
@ -182,33 +182,27 @@ class MCGhidraMixinBase(MCPMixin):
return "default"
# Convenience methods for subclasses
def safe_get(
self, port: int, endpoint: str, params: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
def safe_get(self, port: int, endpoint: str, params: Optional[Dict] = None) -> Dict:
"""Make GET request to Ghidra instance."""
return safe_get(port, endpoint, params)
def safe_post(self, port: int, endpoint: str, data: Any) -> Dict[str, Any]:
def safe_post(self, port: int, endpoint: str, data: Any) -> Dict:
"""Make POST request to Ghidra instance."""
return safe_post(port, endpoint, data)
def safe_put(
self, port: int, endpoint: str, data: Dict[str, Any]
) -> Dict[str, Any]:
def safe_put(self, port: int, endpoint: str, data: Dict) -> Dict:
"""Make PUT request to Ghidra instance."""
return safe_put(port, endpoint, data)
def safe_patch(
self, port: int, endpoint: str, data: Dict[str, Any]
) -> Dict[str, Any]:
def safe_patch(self, port: int, endpoint: str, data: Dict) -> Dict:
"""Make PATCH request to Ghidra instance."""
return safe_patch(port, endpoint, data)
def safe_delete(self, port: int, endpoint: str) -> Dict[str, Any]:
def safe_delete(self, port: int, endpoint: str) -> Dict:
"""Make DELETE request to Ghidra instance."""
return safe_delete(port, endpoint)
def simplify_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
def simplify_response(self, response: Dict) -> Dict:
"""Simplify HATEOAS response."""
return simplify_response(response)

View File

@ -1,4 +1,4 @@
"""Bookmarks mixin for MCGhidra.
"""Bookmarks mixin for GhydraMCP.
Provides tools for managing Ghidra bookmarks (annotations at addresses).
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class BookmarksMixin(MCGhidraMixinBase):
class BookmarksMixin(GhydraMixinBase):
"""Mixin for bookmark operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Cursor management mixin for MCGhidra.
"""Cursor management mixin for GhydraMCP.
Provides tools for managing pagination cursors.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
from ..core.pagination import get_cursor_manager
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class CursorsMixin(MCGhidraMixinBase):
class CursorsMixin(GhydraMixinBase):
"""Mixin for cursor management.
Provides tools for navigating paginated results.

View File

@ -1,4 +1,4 @@
"""Data mixin for MCGhidra.
"""Data mixin for GhydraMCP.
Provides tools for data items and strings operations.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class DataMixin(MCGhidraMixinBase):
class DataMixin(GhydraMixinBase):
"""Mixin for data operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Data types mixin for MCGhidra.
"""Data types mixin for GhydraMCP.
Provides tools for managing enum and typedef data types.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class DataTypesMixin(MCGhidraMixinBase):
class DataTypesMixin(GhydraMixinBase):
"""Mixin for enum and typedef data type operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Docker management mixin for MCGhidra.
"""Docker management mixin for GhydraMCP.
Provides tools for managing Ghidra Docker containers programmatically.
Allows the MCP server to automatically start containers when Ghidra isn't available.
@ -11,7 +11,6 @@ import asyncio
import fcntl
import json
import os
import re
import shutil
import subprocess
import time
@ -20,19 +19,16 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_tool
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from mcghidra.core.logging import logger
from mcghidra.mixins.base import MCGhidraMixinBase
# Port pool configuration — 128 ports by default, configurable via env vars
PORT_POOL_START = int(os.environ.get("MCGHIDRA_PORT_START", "8192"))
PORT_POOL_END = int(os.environ.get("MCGHIDRA_PORT_END", "8319"))
PORT_LOCK_DIR = Path(os.environ.get("MCGHIDRA_PORT_LOCK_DIR", "/tmp/mcghidra-ports"))
# Port pool configuration (32 ports should handle many concurrent sessions)
PORT_POOL_START = 8192
PORT_POOL_END = 8223
PORT_LOCK_DIR = Path("/tmp/ghydramcp-ports")
class PortPool:
"""Manages a pool of ports for MCGhidra containers.
"""Manages a pool of ports for GhydraMCP containers.
Uses file-based locking to coordinate port allocation across multiple
processes. Each allocated port gets a lock file that persists until
@ -67,7 +63,6 @@ class PortPool:
True if port was acquired, False if already in use
"""
lock_path = self._lock_file(port)
fd = None
try:
# Open or create the lock file
@ -82,8 +77,6 @@ class PortPool:
return False
# Write session info to the lock file
# If this fails, we need to close fd to release the lock
try:
os.ftruncate(fd, 0)
os.lseek(fd, 0, os.SEEK_SET)
lock_data = json.dumps({
@ -92,10 +85,6 @@ class PortPool:
"timestamp": time.time(),
})
os.write(fd, lock_data.encode())
except Exception:
# Write failed - release the lock
os.close(fd)
raise
# Keep the file descriptor open to maintain the lock
# Store it so we can release later
@ -105,8 +94,7 @@ class PortPool:
return True
except Exception as e:
logger.debug("Failed to acquire port %d: %s", port, e)
except Exception:
return False
def allocate(self, session_id: str) -> Optional[int]:
@ -146,8 +134,7 @@ class PortPool:
lock_path.unlink()
return True
except Exception as e:
logger.debug("Failed to release port %d: %s", port, e)
except Exception:
return False
def get_allocated_ports(self) -> Dict[int, Dict[str, Any]]:
@ -204,20 +191,20 @@ class PortPool:
except (IOError, OSError):
# Still locked by another process
os.close(fd)
except Exception as e:
logger.debug("Failed to check stale lock for port %d: %s", port, e)
except Exception:
pass
return cleaned
class DockerMixin(MCGhidraMixinBase):
"""Docker container management for MCGhidra.
class DockerMixin(MCPMixin):
"""Docker container management for GhydraMCP.
Provides tools to start, stop, and manage Ghidra containers
with the MCGhidra plugin pre-installed.
with the GhydraMCP plugin pre-installed.
Supports multi-process environments with:
- Dynamic port allocation from a pool ({PORT_POOL_START}-{PORT_POOL_END})
- Dynamic port allocation from a pool (8192-8223)
- Session-scoped container naming with UUIDs
- Docker label-based tracking for cross-process visibility
- Automatic cleanup of orphaned containers
@ -232,14 +219,14 @@ class DockerMixin(MCGhidraMixinBase):
# Track containers started by this session
_session_containers: Dict[str, Dict[str, Any]] = {}
# Label prefix for MCGhidra containers
LABEL_PREFIX = "com.mcghidra"
# Label prefix for GhydraMCP containers
LABEL_PREFIX = "com.ghydramcp"
def __init__(self):
"""Initialize Docker mixin with session isolation."""
self._check_docker_available()
self._session_id = str(uuid.uuid4())[:8]
self._port_pool = None # Lazy-init to avoid side effects
self._port_pool = PortPool()
self._session_containers = {}
@property
@ -249,25 +236,14 @@ class DockerMixin(MCGhidraMixinBase):
self._session_id = str(uuid.uuid4())[:8]
return self._session_id
@property
def port_pool(self) -> PortPool:
"""Get the port pool, creating it on first access.
Lazy initialization avoids creating /tmp/mcghidra-ports
until Docker tools are actually used.
"""
if self._port_pool is None:
self._port_pool = PortPool()
return self._port_pool
def _check_docker_available(self) -> bool:
"""Check if Docker is available on the system."""
return shutil.which("docker") is not None
def _run_docker_cmd_sync(
def _run_docker_cmd(
self, args: List[str], check: bool = True, capture: bool = True
) -> subprocess.CompletedProcess:
"""Run a docker command synchronously (internal use only).
"""Run a docker command.
Args:
args: Command arguments (after 'docker')
@ -285,25 +261,6 @@ class DockerMixin(MCGhidraMixinBase):
text=True,
)
async def _run_docker_cmd(
self, args: List[str], check: bool = True, capture: bool = True
) -> subprocess.CompletedProcess:
"""Run a docker command without blocking the event loop.
Uses run_in_executor to run subprocess in thread pool.
Args:
args: Command arguments (after 'docker')
check: Raise exception on non-zero exit
capture: Capture stdout/stderr
Returns:
CompletedProcess result
"""
return await asyncio.get_running_loop().run_in_executor(
None, self._run_docker_cmd_sync, args, check, capture
)
def _run_compose_cmd(
self,
args: List[str],
@ -332,7 +289,7 @@ class DockerMixin(MCGhidraMixinBase):
env = os.environ.copy()
if project_dir:
env["COMPOSE_PROJECT_NAME"] = "mcghidra"
env["COMPOSE_PROJECT_NAME"] = "ghydramcp"
return subprocess.run(
cmd,
@ -346,7 +303,7 @@ class DockerMixin(MCGhidraMixinBase):
def _generate_container_name(self, binary_name: str) -> str:
"""Generate a unique container name for this session.
Format: mcghidra-{session_id}-{binary_stem}
Format: ghydramcp-{session_id}-{binary_stem}
Args:
binary_name: Name of the binary being analyzed
@ -357,7 +314,7 @@ class DockerMixin(MCGhidraMixinBase):
# Clean binary name for container naming
stem = Path(binary_name).stem.lower()
clean_name = "".join(c if c.isalnum() else "-" for c in stem)[:20]
return f"mcghidra-{self.session_id}-{clean_name}"
return f"ghydramcp-{self.session_id}-{clean_name}"
def _get_container_labels(self, binary_path: str, port: int) -> Dict[str, str]:
"""Generate Docker labels for a container.
@ -379,34 +336,12 @@ class DockerMixin(MCGhidraMixinBase):
f"{self.LABEL_PREFIX}.pid": str(os.getpid()),
}
@staticmethod
def _validate_ghidra_language(language: str) -> bool:
"""Validate Ghidra language ID format (e.g., ARM:LE:32:v4t)."""
return bool(re.match(r'^[A-Za-z0-9_]+:[A-Z]{2}:[0-9]+:[A-Za-z0-9._-]+$', language))
@staticmethod
def _validate_hex_address(address: str) -> bool:
"""Validate hex address format (e.g., 0x00000000 or 00000000)."""
if not re.match(r'^(0x)?[0-9a-fA-F]+$', address):
return False
addr_str = address[2:] if address.startswith("0x") else address
try:
val = int(addr_str, 16)
return 0 <= val <= 0xFFFFFFFFFFFFFFFF
except ValueError:
return False
@staticmethod
def _validate_loader_name(loader: str) -> bool:
"""Validate Ghidra loader name (alphanumeric + underscore)."""
return bool(re.match(r'^[A-Za-z0-9_]+$', loader))
async def _find_containers_by_label(
def _find_containers_by_label(
self,
label_filter: Optional[str] = None,
session_only: bool = False,
) -> List[Dict[str, Any]]:
"""Find MCGhidra containers by label.
"""Find GhydraMCP containers by label.
Args:
label_filter: Additional label filter (e.g., "port=8192")
@ -424,7 +359,7 @@ class DockerMixin(MCGhidraMixinBase):
if label_filter:
filter_args.extend(["--filter", f"label={self.LABEL_PREFIX}.{label_filter}"])
ps_result = await self._run_docker_cmd(
ps_result = self._run_docker_cmd(
[
"ps", "-a",
*filter_args,
@ -454,19 +389,19 @@ class DockerMixin(MCGhidraMixinBase):
@mcp_tool(
name="docker_status",
description="Check Docker availability and running MCGhidra containers",
description="Check Docker availability and running GhydraMCP containers",
)
async def docker_status(self, ctx: Optional[Context] = None) -> Dict[str, Any]:
"""Check Docker status and list running MCGhidra containers.
"""Check Docker status and list running GhydraMCP containers.
Returns:
Status information including:
- docker_available: Whether Docker is installed
- docker_running: Whether Docker daemon is running
- session_id: This MCP instance's session ID
- containers: List of MCGhidra containers with their status
- containers: List of GhydraMCP containers with their status
- port_pool: Port allocation status
- images: Available MCGhidra images
- images: Available GhydraMCP images
"""
result = {
"docker_available": False,
@ -490,35 +425,36 @@ class DockerMixin(MCGhidraMixinBase):
# Check if docker daemon is running
try:
await self._run_docker_cmd(["info"], check=True)
self._run_docker_cmd(["info"], check=True)
result["docker_running"] = True
except (subprocess.CalledProcessError, FileNotFoundError):
return result
# Check for docker compose
try:
await self._run_docker_cmd(["compose", "version"], check=True)
self._run_docker_cmd(["compose", "version"], check=True)
result["compose_available"] = True
except subprocess.CalledProcessError:
pass
# List all MCGhidra containers (from any session)
result["containers"] = await self._find_containers_by_label()
# List all GhydraMCP containers (from any session)
result["containers"] = self._find_containers_by_label()
# List containers from this session only
result["session_containers"] = await self._find_containers_by_label(session_only=True)
result["session_containers"] = self._find_containers_by_label(session_only=True)
# Get port pool status (lazy-init creates pool on first access)
result["port_pool"]["allocated"] = self.port_pool.get_allocated_ports()
# Get port pool status
if self._port_pool:
result["port_pool"]["allocated"] = self._port_pool.get_allocated_ports()
# Also check by name pattern for containers without labels
try:
ps_result = await self._run_docker_cmd(
ps_result = self._run_docker_cmd(
[
"ps",
"-a",
"--filter",
"name=mcghidra",
"name=ghydramcp",
"--format",
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}",
]
@ -540,13 +476,13 @@ class DockerMixin(MCGhidraMixinBase):
except subprocess.CalledProcessError:
pass
# List MCGhidra images
# List GhydraMCP images
try:
images_result = await self._run_docker_cmd(
images_result = self._run_docker_cmd(
[
"images",
"--filter",
"reference=mcghidra*",
"reference=ghydramcp*",
"--format",
"{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}",
]
@ -569,25 +505,22 @@ class DockerMixin(MCGhidraMixinBase):
@mcp_tool(
name="docker_start",
description="Start a MCGhidra Docker container to analyze a binary (auto-assigns port from pool)",
description="Start a GhydraMCP Docker container to analyze a binary (auto-assigns port from pool)",
)
async def docker_start(
self,
binary_path: str,
memory: str = "2G",
name: Optional[str] = None,
language: Optional[str] = None,
base_address: Optional[str] = None,
loader: Optional[str] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Start a MCGhidra Docker container for binary analysis.
"""Start a GhydraMCP Docker container for binary analysis.
This creates a new Ghidra instance in Docker with the MCGhidra
This creates a new Ghidra instance in Docker with the GhydraMCP
plugin pre-installed. The binary will be imported and analyzed,
then the HTTP API will be available.
Ports are automatically allocated from the pool ({PORT_POOL_START}-{PORT_POOL_END}) to
Ports are automatically allocated from the pool (8192-8223) to
prevent conflicts between concurrent sessions. Container names
are auto-generated with the session ID to ensure uniqueness.
@ -595,9 +528,6 @@ class DockerMixin(MCGhidraMixinBase):
binary_path: Path to the binary file to analyze
memory: Max JVM heap memory (default: 2G)
name: Container name (auto-generated if not specified)
language: Ghidra processor language ID for raw binaries (e.g., "ARM:LE:32:v4t")
base_address: Base address for raw binaries (e.g., "0x00000000")
loader: Ghidra loader type (e.g., "BinaryLoader"). Auto-set when language is specified.
Returns:
Container info including ID, name, port, and API URL
@ -610,6 +540,14 @@ class DockerMixin(MCGhidraMixinBase):
if not binary_file.exists():
return {"error": f"Binary not found: {binary_path}"}
# Always allocate from pool to prevent conflicts between sessions
port = self._port_pool.allocate(self.session_id)
if port is None:
return {
"error": "Port pool exhausted (8192-8223). Stop some containers first.",
"allocated_ports": self._port_pool.get_allocated_ports(),
}
# Generate container name if not specified
if name is None:
name = self._generate_container_name(binary_file.name)
@ -617,51 +555,25 @@ class DockerMixin(MCGhidraMixinBase):
# Clean up invalid characters in container name
name = "".join(c if c.isalnum() or c in "-_" else "-" for c in name)
port = None
try:
# Check if container with this name already exists
check_result = await self._run_docker_cmd(
check_result = self._run_docker_cmd(
["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False
)
if check_result.stdout.strip():
self._port_pool.release(port)
return {
"error": f"Container '{name}' already exists. Stop it first with docker_stop."
}
# Allocate a port that's both lockable AND not in use by Docker.
# We HOLD flocks on Docker-occupied ports while searching, so
# allocate() advances past them. Release held ports after.
held_ports = [] # Ports we locked but can't use (Docker-occupied)
try:
for _ in range(PORT_POOL_END - PORT_POOL_START + 1):
candidate_port = self.port_pool.allocate(self.session_id)
if candidate_port is None:
break # Pool exhausted
# Check if this port is already in use by a Docker container
port_check = await self._run_docker_cmd(
["ps", "-q", "-f", f"publish={candidate_port}"], check=False
# Check if port is already in use by a non-pool container
port_check = self._run_docker_cmd(
["ps", "-q", "-f", f"publish={port}"], check=False
)
if port_check.stdout.strip():
# Port is Docker-occupied — hold the flock so allocate()
# skips it on the next iteration, then release after loop
held_ports.append(candidate_port)
continue
# Found a usable port!
port = candidate_port
break
finally:
# Release all the Docker-occupied ports we held during the scan
for held in held_ports:
self.port_pool.release(held)
if port is None:
self._port_pool.release(port)
return {
"error": f"Port pool exhausted ({PORT_POOL_START}-{PORT_POOL_END}). All ports are in use.",
"docker_occupied": held_ports if held_ports else [],
"hint": "Stop some containers with docker_stop or docker_cleanup.",
"allocated_ports": self.port_pool.get_allocated_ports(),
"error": f"Port {port} is already in use by another container"
}
# Build label arguments
@ -670,40 +582,8 @@ class DockerMixin(MCGhidraMixinBase):
for k, v in labels.items():
label_args.extend(["-l", f"{k}={v}"])
# Validate firmware import parameters
if language and not self._validate_ghidra_language(language):
self.port_pool.release(port)
return {
"error": f"Invalid language format: {language}",
"hint": "Expected ARCH:ENDIAN:SIZE:VARIANT (e.g., ARM:LE:32:v4t)",
}
if base_address and not self._validate_hex_address(base_address):
self.port_pool.release(port)
return {
"error": f"Invalid base address: {base_address}",
"hint": "Expected hex format: 0x00000000 or 00000000",
}
if loader and not self._validate_loader_name(loader):
self.port_pool.release(port)
return {
"error": f"Invalid loader name: {loader}",
"hint": "Expected alphanumeric name (e.g., BinaryLoader)",
}
# Build environment variable arguments
env_args = ["-e", f"MCGHIDRA_MAXMEM={memory}"]
if language:
env_args.extend(["-e", f"GHIDRA_LANGUAGE={language}"])
# Auto-set BinaryLoader when language is explicitly specified
if not loader:
loader = "BinaryLoader"
if base_address:
env_args.extend(["-e", f"GHIDRA_BASE_ADDRESS={base_address}"])
if loader:
env_args.extend(["-e", f"GHIDRA_LOADER={loader}"])
# Start the container
run_result = await self._run_docker_cmd(
run_result = self._run_docker_cmd(
[
"run",
"-d",
@ -713,9 +593,10 @@ class DockerMixin(MCGhidraMixinBase):
f"{port}:8192",
"-v",
f"{binary_file.parent}:/binaries:ro",
*env_args,
"-e",
f"GHYDRA_MAXMEM={memory}",
*label_args,
"mcghidra:latest",
"ghydramcp:latest",
f"/binaries/{binary_file.name}",
]
)
@ -723,19 +604,14 @@ class DockerMixin(MCGhidraMixinBase):
container_id = run_result.stdout.strip()
# Track the container in this session
session_info: Dict[str, Any] = {
self._session_containers[container_id] = {
"name": name,
"port": port,
"binary": str(binary_file),
"memory": memory,
}
if language:
session_info["language"] = language
if base_address:
session_info["base_address"] = base_address
self._session_containers[container_id] = session_info
result_info: Dict[str, Any] = {
return {
"success": True,
"session_id": self.session_id,
"container_id": container_id[:12],
@ -749,25 +625,19 @@ class DockerMixin(MCGhidraMixinBase):
f"Use docker_logs('{name}') to monitor progress."
),
}
if language:
result_info["language"] = language
if base_address:
result_info["base_address"] = base_address
return result_info
except subprocess.CalledProcessError as e:
if port is not None:
self.port_pool.release(port)
self._port_pool.release(port)
return {"error": f"Failed to start container: {e.stderr or e.stdout}"}
@mcp_tool(
name="docker_stop",
description="Stop a running MCGhidra Docker container",
description="Stop a running GhydraMCP Docker container",
)
async def docker_stop(
self, name_or_id: str, remove: bool = True, ctx: Optional[Context] = None
) -> Dict[str, Any]:
"""Stop a MCGhidra Docker container.
"""Stop a GhydraMCP Docker container.
For safety, this will only stop containers that belong to the current
MCP session. Attempting to stop another session's container will fail
@ -787,7 +657,7 @@ class DockerMixin(MCGhidraMixinBase):
container_port = None
container_session = None
try:
inspect_result = await self._run_docker_cmd(
inspect_result = self._run_docker_cmd(
[
"inspect",
"--format",
@ -813,14 +683,14 @@ class DockerMixin(MCGhidraMixinBase):
try:
# Stop the container
await self._run_docker_cmd(["stop", name_or_id])
self._run_docker_cmd(["stop", name_or_id])
if remove:
await self._run_docker_cmd(["rm", name_or_id])
self._run_docker_cmd(["rm", name_or_id])
# Release the port back to the pool
if container_port:
self.port_pool.release(container_port)
self._port_pool.release(container_port)
# Remove from session tracking
self._session_containers = {
@ -841,7 +711,7 @@ class DockerMixin(MCGhidraMixinBase):
@mcp_tool(
name="docker_logs",
description="Get logs from a MCGhidra Docker container",
description="Get logs from a GhydraMCP Docker container",
)
async def docker_logs(
self,
@ -850,7 +720,7 @@ class DockerMixin(MCGhidraMixinBase):
follow: bool = False,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get logs from a MCGhidra container.
"""Get logs from a GhydraMCP container.
Args:
name_or_id: Container name or ID
@ -869,7 +739,7 @@ class DockerMixin(MCGhidraMixinBase):
args.append("-f")
args.append(name_or_id)
result = await self._run_docker_cmd(args)
result = self._run_docker_cmd(args)
return {
"success": True,
"container": name_or_id,
@ -881,7 +751,7 @@ class DockerMixin(MCGhidraMixinBase):
@mcp_tool(
name="docker_build",
description="Build the MCGhidra Docker image from source",
description="Build the GhydraMCP Docker image from source",
)
async def docker_build(
self,
@ -890,12 +760,12 @@ class DockerMixin(MCGhidraMixinBase):
project_dir: Optional[str] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Build the MCGhidra Docker image.
"""Build the GhydraMCP Docker image.
Args:
tag: Image tag (default: 'latest')
no_cache: Build without using cache
project_dir: Path to MCGhidra project (auto-detected if not specified)
project_dir: Path to GhydraMCP project (auto-detected if not specified)
Returns:
Build status
@ -913,7 +783,7 @@ class DockerMixin(MCGhidraMixinBase):
proj_path = module_dir
else:
return {
"error": "Could not find MCGhidra project directory. Please specify project_dir."
"error": "Could not find GhydraMCP project directory. Please specify project_dir."
}
dockerfile = proj_path / "docker" / "Dockerfile"
@ -924,7 +794,7 @@ class DockerMixin(MCGhidraMixinBase):
args = [
"build",
"-t",
f"mcghidra:{tag}",
f"ghydramcp:{tag}",
"-f",
str(dockerfile),
]
@ -933,12 +803,12 @@ class DockerMixin(MCGhidraMixinBase):
args.append(str(proj_path))
# Run build (this can take a while)
result = await self._run_docker_cmd(args, capture=True)
result = self._run_docker_cmd(args, capture=True)
return {
"success": True,
"image": f"mcghidra:{tag}",
"message": f"Successfully built mcghidra:{tag}",
"image": f"ghydramcp:{tag}",
"message": f"Successfully built ghydramcp:{tag}",
"output": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout,
}
@ -959,25 +829,19 @@ class DockerMixin(MCGhidraMixinBase):
import urllib.error
import urllib.request
health_url = f"http://localhost:{port}/health"
root_url = f"http://localhost:{port}/"
url = f"http://localhost:{port}/"
try:
# Try /health first (available in plugin v2.2+)
req = urllib.request.Request(health_url)
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=timeout) as response:
data = json_module.loads(response.read().decode())
result = data.get("result", data)
return {
"healthy": True,
"port": port,
"api_version": result.get("api_version"),
"program": result.get("program"),
"uptime_ms": result.get("uptime_ms"),
"api_version": data.get("api_version"),
"program": data.get("program"),
"file": data.get("file"),
}
except urllib.error.HTTPError:
# /health not available — fall back to root endpoint (older plugin)
pass
except urllib.error.URLError as e:
return {
"healthy": False,
@ -992,55 +856,77 @@ class DockerMixin(MCGhidraMixinBase):
"error": str(e),
}
# Fallback: try root endpoint for older plugin versions
try:
req = urllib.request.Request(root_url)
with urllib.request.urlopen(req, timeout=timeout) as response:
data = json_module.loads(response.read().decode())
return {
"healthy": True,
"port": port,
"api_version": data.get("api_version"),
"program": data.get("program"),
}
except Exception as e:
return {
"healthy": False,
"port": port,
"error": str(e),
}
@mcp_tool(
name="docker_health",
description="Check if a MCGhidra container's API is responding",
description="Check if a GhydraMCP container's API is responding",
)
async def docker_health(
self, port: Optional[int] = None, timeout: float = 5.0, ctx: Optional[Context] = None
self, port: int = 8192, timeout: float = 5.0, ctx: Optional[Context] = None
) -> Dict[str, Any]:
"""Check if a MCGhidra container's API is healthy.
"""Check if a GhydraMCP container's API is healthy.
Args:
port: API port to check (uses current instance if not specified)
port: API port to check (default: 8192)
timeout: Request timeout in seconds
Returns:
Health status and API info if available
"""
port = self.get_instance_port(port)
return await asyncio.get_running_loop().run_in_executor(
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None, self._sync_health_check, port, timeout
)
@mcp_tool(
name="docker_wait",
description="Wait for a GhydraMCP container to become healthy",
)
async def docker_wait(
self,
port: int = 8192,
timeout: float = 300.0,
interval: float = 5.0,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Wait for a GhydraMCP container to become healthy.
Polls the API endpoint until it responds or timeout is reached.
Args:
port: API port to check (default: 8192)
timeout: Maximum time to wait in seconds (default: 300)
interval: Polling interval in seconds (default: 5)
Returns:
Health status once healthy, or error on timeout
"""
start_time = time.time()
last_error = None
while (time.time() - start_time) < timeout:
result = await self.docker_health(port=port, timeout=interval, ctx=ctx)
if result.get("healthy"):
result["waited_seconds"] = round(time.time() - start_time, 1)
return result
last_error = result.get("error")
await asyncio.sleep(interval)
return {
"healthy": False,
"port": port,
"error": f"Timeout after {timeout}s waiting for container",
"last_error": last_error,
}
@mcp_tool(
name="docker_auto_start",
description="Automatically start a MCGhidra container with dynamic port allocation",
description="Automatically start a GhydraMCP container with dynamic port allocation",
)
async def docker_auto_start(
self,
binary_path: str,
language: Optional[str] = None,
base_address: Optional[str] = None,
loader: Optional[str] = None,
wait: bool = False,
timeout: float = 300.0,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Automatically start a Docker container with intelligent port allocation.
@ -1048,23 +934,19 @@ class DockerMixin(MCGhidraMixinBase):
This is the main entry point for automatic Docker management:
1. Checks if a Ghidra instance with the SAME binary is already running
2. If not, allocates a port from the pool and starts a new container
3. Returns connection info immediately
3. Optionally waits for the container to become healthy
4. Returns connection info for the instance
Ports are auto-allocated from the pool ({PORT_POOL_START}-{PORT_POOL_END}) to prevent
Ports are auto-allocated from the pool (8192-8223) to prevent
conflicts between concurrent sessions.
After starting, poll docker_health(port) in a loop to check readiness.
This gives you visibility into progress and ability to check logs.
Args:
binary_path: Path to the binary to analyze
language: Ghidra processor language ID for raw binaries (e.g., "ARM:LE:32:v4t")
base_address: Base address for raw binaries (e.g., "0x00000000")
loader: Ghidra loader type (e.g., "BinaryLoader"). Auto-set when language is specified.
wait: Wait for container to be ready (default: False, use docker_wait separately)
timeout: Max wait time in seconds (default: 300)
Returns:
Instance connection info with session ID and port details.
Poll docker_health(port) to check when container is ready.
Instance connection info with session ID and port details
"""
import os
@ -1097,21 +979,17 @@ class DockerMixin(MCGhidraMixinBase):
}
# Check if we have the image
if not any("mcghidra" in img.get("name", "") for img in status.get("images", [])):
if not any("ghydramcp" in img.get("name", "") for img in status.get("images", [])):
return {
"error": (
"MCGhidra Docker image not found. "
"GhydraMCP Docker image not found. "
"Build it with docker_build() or 'make build' first."
)
}
# Start a new container (port auto-allocated from pool)
start_result = await self.docker_start(
binary_path=binary_path,
language=language,
base_address=base_address,
loader=loader,
ctx=ctx,
binary_path=binary_path, ctx=ctx
)
if not start_result.get("success"):
@ -1119,6 +997,31 @@ class DockerMixin(MCGhidraMixinBase):
actual_port = start_result.get("port")
if wait:
# Wait for the container to become healthy
wait_result = await self.docker_wait(port=actual_port, timeout=timeout, ctx=ctx)
if wait_result.get("healthy"):
return {
"source": "docker",
"session_id": self.session_id,
"container_id": start_result.get("container_id"),
"container_name": start_result.get("name"),
"port": actual_port,
"api_url": f"http://localhost:{actual_port}/",
"program": wait_result.get("program"),
"waited_seconds": wait_result.get("waited_seconds"),
"message": f"Docker container ready on port {actual_port} after {wait_result.get('waited_seconds')}s",
}
else:
return {
"warning": "Container started but not yet healthy",
"session_id": self.session_id,
"container_id": start_result.get("container_id"),
"port": actual_port,
"last_error": wait_result.get("error"),
"message": "Container may still be analyzing. Check docker_logs() for progress.",
}
return {
"source": "docker",
"session_id": self.session_id,
@ -1126,7 +1029,7 @@ class DockerMixin(MCGhidraMixinBase):
"container_name": start_result.get("name"),
"port": actual_port,
"api_url": f"http://localhost:{actual_port}/",
"message": f"Container starting on port {actual_port}. Poll docker_health(port={actual_port}), then call instances_use(port={actual_port}) when healthy.",
"message": f"Container starting on port {actual_port}. Use docker_wait() or docker_health() to check status.",
}
@mcp_tool(
@ -1140,14 +1043,14 @@ class DockerMixin(MCGhidraMixinBase):
dry_run: bool = False,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Clean up orphaned MCGhidra containers and stale port locks.
"""Clean up orphaned GhydraMCP containers and stale port locks.
This helps recover from crashed processes that left containers or
port locks behind.
By default, only cleans containers from the current session to prevent
accidentally removing another agent's work. Set session_only=False
(with caution) to clean all MCGhidra containers.
(with caution) to clean all GhydraMCP containers.
Args:
session_only: Only clean up containers from this session (default: True for safety)
@ -1168,12 +1071,12 @@ class DockerMixin(MCGhidraMixinBase):
}
# Find orphaned containers
containers = await self._find_containers_by_label(session_only=session_only)
containers = self._find_containers_by_label(session_only=session_only)
for container in containers:
# Check if container is old enough to be considered orphaned
try:
inspect_result = await self._run_docker_cmd(
inspect_result = self._run_docker_cmd(
["inspect", "--format", "{{index .Config.Labels \"" + self.LABEL_PREFIX + ".started\"}}", container["id"]],
check=False,
)
@ -1203,7 +1106,8 @@ class DockerMixin(MCGhidraMixinBase):
pass
# Clean up stale port locks
stale_ports = self.port_pool.cleanup_stale_locks(max_age_hours * 3600)
if self._port_pool:
stale_ports = self._port_pool.cleanup_stale_locks(max_age_hours * 3600)
result["ports_cleaned"] = stale_ports
return result
@ -1228,8 +1132,8 @@ class DockerMixin(MCGhidraMixinBase):
"containers": self._session_containers,
"allocated_ports": {
port: info
for port, info in self.port_pool.get_allocated_ports().items()
for port, info in self._port_pool.get_allocated_ports().items()
if info.get("session_id") == self.session_id
},
} if self._port_pool else {},
"port_pool_range": f"{PORT_POOL_START}-{PORT_POOL_END}",
}

View File

@ -1,4 +1,4 @@
"""Functions mixin for MCGhidra.
"""Functions mixin for GhydraMCP.
Provides tools for function analysis, decompilation, and manipulation.
"""
@ -10,10 +10,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class FunctionsMixin(MCGhidraMixinBase):
class FunctionsMixin(GhydraMixinBase):
"""Mixin for function operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Instance management mixin for MCGhidra.
"""Instance management mixin for GhydraMCP.
Provides tools for discovering, registering, and managing Ghidra instances.
"""
@ -9,10 +9,10 @@ from typing import Any, Dict, Optional
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class InstancesMixin(MCGhidraMixinBase):
class InstancesMixin(GhydraMixinBase):
"""Mixin for Ghidra instance management.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Memory mixin for MCGhidra.
"""Memory mixin for GhydraMCP.
Provides tools for memory read/write operations.
"""
@ -7,10 +7,10 @@ from typing import Any, Dict, Optional
from fastmcp.contrib.mcp_mixin import mcp_tool
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class MemoryMixin(MCGhidraMixinBase):
class MemoryMixin(GhydraMixinBase):
"""Mixin for memory operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Namespaces mixin for MCGhidra.
"""Namespaces mixin for GhydraMCP.
Provides tools for querying namespaces and class definitions.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class NamespacesMixin(MCGhidraMixinBase):
class NamespacesMixin(GhydraMixinBase):
"""Mixin for namespace and class operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Segments mixin for MCGhidra.
"""Segments mixin for GhydraMCP.
Provides tools for querying memory segments (sections) and their permissions.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class SegmentsMixin(MCGhidraMixinBase):
class SegmentsMixin(GhydraMixinBase):
"""Mixin for memory segment operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Structs mixin for MCGhidra.
"""Structs mixin for GhydraMCP.
Provides tools for struct data type operations.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class StructsMixin(MCGhidraMixinBase):
class StructsMixin(GhydraMixinBase):
"""Mixin for struct operations.
Provides tools for:
@ -99,7 +99,7 @@ class StructsMixin(MCGhidraMixinBase):
grep: Optional[str] = None,
grep_ignorecase: bool = True,
return_all: bool = False,
fields: Optional[List[str]] = None,
project_fields: Optional[List[str]] = None,
ctx: Optional[Context] = None,
) -> Dict[str, Any]:
"""Get detailed information about a struct with field pagination.
@ -111,7 +111,7 @@ class StructsMixin(MCGhidraMixinBase):
grep: Regex pattern to filter fields
grep_ignorecase: Case-insensitive grep (default: True)
return_all: Return all fields without pagination
fields: Field names to keep per struct field item. Reduces response size.
project_fields: Field names to keep per struct field item. Reduces response size.
ctx: FastMCP context (auto-injected)
Returns:
@ -145,17 +145,17 @@ class StructsMixin(MCGhidraMixinBase):
# Extract struct info and fields
struct_info = {}
struct_fields = []
fields = []
if isinstance(result, dict):
for key, value in result.items():
if key == "fields" and isinstance(value, list):
struct_fields = value
fields = value
else:
struct_info[key] = value
# If few fields and no grep, return as-is
if len(struct_fields) <= 10 and not grep:
if len(fields) <= 10 and not grep:
return simplified
query_params = {
@ -166,7 +166,7 @@ class StructsMixin(MCGhidraMixinBase):
# Paginate fields
paginated = self.filtered_paginate(
data=struct_fields,
data=fields,
query_params=query_params,
tool_name="structs_get",
session_id=session_id,
@ -174,7 +174,7 @@ class StructsMixin(MCGhidraMixinBase):
grep=grep,
grep_ignorecase=grep_ignorecase,
return_all=return_all,
fields=fields,
fields=project_fields,
)
# Merge struct metadata with paginated fields (skip if guarded)

View File

@ -1,4 +1,4 @@
"""Symbols mixin for MCGhidra.
"""Symbols mixin for GhydraMCP.
Provides tools for symbol table operations including labels, imports, and exports.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class SymbolsMixin(MCGhidraMixinBase):
class SymbolsMixin(GhydraMixinBase):
"""Mixin for symbol table operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Variables mixin for MCGhidra.
"""Variables mixin for GhydraMCP.
Provides tools for querying global and function-local variables.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class VariablesMixin(MCGhidraMixinBase):
class VariablesMixin(GhydraMixinBase):
"""Mixin for variable operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""Cross-references mixin for MCGhidra.
"""Cross-references mixin for GhydraMCP.
Provides tools for cross-reference (xref) operations.
"""
@ -9,10 +9,10 @@ from fastmcp import Context
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config
from .base import MCGhidraMixinBase
from .base import GhydraMixinBase
class XrefsMixin(MCGhidraMixinBase):
class XrefsMixin(GhydraMixinBase):
"""Mixin for cross-reference operations.
Provides tools for:

View File

@ -1,4 +1,4 @@
"""MCGhidra Server - FastMCP server composing all mixins.
"""GhydraMCP Server - FastMCP server composing all mixins.
This module creates and configures the FastMCP server by composing
all domain-specific mixins into a single MCP server.
@ -13,8 +13,7 @@ from typing import Optional
from fastmcp import FastMCP
from .config import MCGhidraConfig, get_config, set_config
from .core.logging import configure_logging
from .config import GhydraConfig, get_config, set_config
from .mixins import (
AnalysisMixin,
BookmarksMixin,
@ -35,10 +34,10 @@ from .mixins import (
def create_server(
name: str = "MCGhidra",
config: Optional[MCGhidraConfig] = None,
name: str = "GhydraMCP",
config: Optional[GhydraConfig] = None,
) -> FastMCP:
"""Create and configure the MCGhidra server.
"""Create and configure the GhydraMCP server.
Args:
name: Server name
@ -114,7 +113,7 @@ def _periodic_discovery(interval: int = 30):
"""
import requests as _requests
from .mixins.base import MCGhidraMixinBase
from .mixins.base import GhydraMixinBase
config = get_config()
@ -133,9 +132,9 @@ def _periodic_discovery(interval: int = 30):
if resp.ok:
response = resp.json()
if response.get("success", False):
with MCGhidraMixinBase._instances_lock:
if port not in MCGhidraMixinBase._instances:
MCGhidraMixinBase._instances[port] = {
with GhydraMixinBase._instances_lock:
if port not in GhydraMixinBase._instances:
GhydraMixinBase._instances[port] = {
"url": url.rstrip("/"),
"project": response.get("project", ""),
"file": response.get("file", ""),
@ -149,28 +148,22 @@ def _periodic_discovery(interval: int = 30):
def _handle_sigint(signum, frame):
"""Handle SIGINT gracefully."""
print("\nShutting down MCGhidra...", file=sys.stderr)
print("\nShutting down GhydraMCP...", file=sys.stderr)
sys.exit(0)
def main():
"""Main entry point for the MCGhidra server."""
import logging
import os
"""Main entry point for the GhydraMCP server."""
import shutil
# Configure logging early (DEBUG if MCGHIDRAMCP_DEBUG is set)
log_level = logging.DEBUG if os.environ.get("MCGHIDRAMCP_DEBUG") else logging.INFO
configure_logging(log_level)
try:
from importlib.metadata import version
package_version = version("mcghidra")
package_version = version("ghydramcp")
except Exception:
package_version = "2025.12.1"
print(f"🔬 MCGhidra v{package_version}", file=sys.stderr)
print(" Reverse engineering bridge for Ghidra", file=sys.stderr)
print(f"🔬 GhydraMCP v{package_version}", file=sys.stderr)
print(" AI-assisted reverse engineering bridge for Ghidra", file=sys.stderr)
# Check Docker availability
docker_available = shutil.which("docker") is not None
@ -191,15 +184,14 @@ def main():
print(f" Discovering Ghidra instances on {config.ghidra_host}...", file=sys.stderr)
from .core.http_client import safe_get
from .mixins.base import MCGhidraMixinBase
from .mixins.base import GhydraMixinBase
found = 0
for port in config.quick_discovery_range:
try:
response = safe_get(port, "")
if response.get("success", False):
with MCGhidraMixinBase._instances_lock:
MCGhidraMixinBase._instances[port] = {
GhydraMixinBase._instances[port] = {
"url": f"http://{config.ghidra_host}:{port}",
"project": response.get("project", ""),
"file": response.get("file", ""),
@ -219,7 +211,7 @@ def main():
discovery_thread = threading.Thread(
target=_periodic_discovery,
daemon=True,
name="MCGhidra-Discovery",
name="GhydraMCP-Discovery",
)
discovery_thread.start()

View File

@ -39,26 +39,25 @@ import ghidra.util.Msg;
status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME,
category = PluginCategoryNames.ANALYSIS,
shortDescription = "MCGhidra Plugin for MCP Analysis",
description = "Exposes program data via HATEOAS HTTP API for automated reverse engineering with MCP (Model Context Protocol).",
shortDescription = "GhydraMCP Plugin for AI Analysis",
description = "Exposes program data via HATEOAS HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).",
servicesRequired = { ProgramManager.class }
)
public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
// Made public static to be accessible by InstanceEndpoints
public static final Map<Integer, MCGhidraPlugin> activeInstances = new ConcurrentHashMap<>();
public static final Map<Integer, GhydraMCPPlugin> activeInstances = new ConcurrentHashMap<>();
private static final Object baseInstanceLock = new Object();
private HttpServer server;
private int port;
private boolean isBaseInstance = false;
private long serverStartTimeMs;
/**
* Constructor for MCGhidra Plugin.
* Constructor for GhydraMCP Plugin.
* @param tool The Ghidra PluginTool
*/
public MCGhidraPlugin(PluginTool tool) {
public GhydraMCPPlugin(PluginTool tool) {
super(tool);
this.port = findAvailablePort();
@ -71,8 +70,8 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
}
}
Msg.info(this, "MCGhidraPlugin loaded on port " + port);
System.out.println("[MCGhidra] Plugin loaded on port " + port);
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
try {
startServer();
@ -110,13 +109,11 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
// Register Root Endpoint (should be last to include links to all other endpoints)
registerRootEndpoint(server);
serverStartTimeMs = System.currentTimeMillis();
new Thread(() -> {
server.start();
Msg.info(this, "MCGhidra HTTP server started on port " + port);
System.out.println("[MCGhidra] HTTP server started on port " + port);
}, "MCGhidra-HTTP-Server").start();
Msg.info(this, "GhydraMCP HTTP server started on port " + port);
System.out.println("[GhydraMCP] HTTP server started on port " + port);
}, "GhydraMCP-HTTP-Server").start();
}
/**
@ -187,35 +184,6 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
}
});
// Health endpoint lightweight, no program dependency
server.createContext("/health", exchange -> {
try {
if ("GET".equals(exchange.getRequestMethod())) {
long uptimeMs = System.currentTimeMillis() - serverStartTimeMs;
Program program = getCurrentProgram();
Map<String, Object> healthData = new HashMap<>();
healthData.put("status", "up");
healthData.put("port", port);
healthData.put("api_version", ApiConstants.API_VERSION);
healthData.put("uptime_ms", uptimeMs);
healthData.put("program", program != null ? program.getName() : null);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(healthData)
.addLink("self", "/health")
.addLink("root", "/");
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} else {
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
}
} catch (IOException e) {
Msg.error(this, "Error handling /health", e);
}
});
// Info endpoint
server.createContext("/info", exchange -> {
try {
@ -382,7 +350,7 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
}
Map<String, Object> rootData = new HashMap<>();
rootData.put("message", "MCGhidra API " + ApiConstants.API_VERSION);
rootData.put("message", "GhydraMCP API " + ApiConstants.API_VERSION);
rootData.put("documentation", "See GHIDRA_HTTP_API.md for full API documentation");
rootData.put("isBaseInstance", isBaseInstance);
@ -391,7 +359,6 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
.success(true)
.result(rootData)
.addLink("self", "/")
.addLink("health", "/health")
.addLink("info", "/info")
.addLink("plugin-version", "/plugin-version")
.addLink("projects", "/projects")
@ -482,8 +449,8 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
public void dispose() {
if (server != null) {
server.stop(0); // Stop immediately
Msg.info(this, "MCGhidra HTTP server stopped on port " + port);
System.out.println("[MCGhidra] HTTP server stopped on port " + port);
Msg.info(this, "GhydraMCP HTTP server stopped on port " + port);
System.out.println("[GhydraMCP] HTTP server stopped on port " + port);
}
activeInstances.remove(port);
super.dispose();

View File

@ -4,7 +4,7 @@ package eu.starsong.ghidra.endpoints;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import eu.starsong.ghidra.api.ResponseBuilder;
import eu.starsong.ghidra.MCGhidraPlugin; // Need access to activeInstances
import eu.starsong.ghidra.GhydraMCPPlugin; // Need access to activeInstances
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
@ -13,16 +13,16 @@ package eu.starsong.ghidra.endpoints;
public class InstanceEndpoints extends AbstractEndpoint {
// Need a way to access the static activeInstances map from MCGhidraPlugin
// Need a way to access the static activeInstances map from GhydraMCPPlugin
// This is a bit awkward and suggests the instance management might need
// a different design, perhaps a dedicated manager class.
// For now, we pass the map or use a static accessor if made public.
private final Map<Integer, MCGhidraPlugin> activeInstances;
private final Map<Integer, GhydraMCPPlugin> activeInstances;
// Note: Passing currentProgram might be null here if no program is open.
// The constructor in AbstractEndpoint handles null program.
// Updated constructor to accept port
public InstanceEndpoints(Program program, int port, Map<Integer, MCGhidraPlugin> instances) {
public InstanceEndpoints(Program program, int port, Map<Integer, GhydraMCPPlugin> instances) {
super(program, port); // Call super constructor
this.activeInstances = instances;
}
@ -46,7 +46,7 @@ package eu.starsong.ghidra.endpoints;
// Accessing the static map directly - requires it to be accessible
// or passed in constructor.
for (Map.Entry<Integer, MCGhidraPlugin> entry : activeInstances.entrySet()) {
for (Map.Entry<Integer, GhydraMCPPlugin> entry : activeInstances.entrySet()) {
Map<String, Object> instance = new HashMap<>();
int instancePort = entry.getKey();
instance.put("port", instancePort);

View File

@ -1,6 +1,6 @@
Manifest-Version: 1.0
Plugin-Class: eu.starsong.ghidra.MCGhidra
Plugin-Name: MCGhidra
Plugin-Class: eu.starsong.ghidra.GhydraMCP
Plugin-Name: GhydraMCP
Plugin-Version: 11.4.2
Bundle-Version: dev-SNAPSHOT
Plugin-Author: LaurieWired, Teal Bauer

View File

@ -1,9 +1,9 @@
# MCGhidra Module Manifest
# GhydraMCP Module Manifest
#
# This file lists third-party libraries bundled with this extension and their licenses.
# Module metadata (name, description, version) is defined in extension.properties.
#
# Format: MODULE FILE LICENSE: lib/filename.jar License Name
#
# Currently, MCGhidra has no bundled third-party libraries.
# Currently, GhydraMCP has no bundled third-party libraries.
# Gson is provided by Ghidra itself.

View File

@ -1,4 +1,4 @@
name=MCGhidra
name=GhydraMCP
description=A multi-headed REST interface for Ghidra for use with MCP agents.
author=Laurie Wired, Teal Bauer
createdOn=2025-03-29

View File

@ -1,15 +0,0 @@
"""MCGhidra - Reverse engineering bridge for Ghidra.
Multi-instance Ghidra plugin with HATEOAS REST API and MCP server
for decompilation, analysis & binary manipulation.
"""
try:
from importlib.metadata import version
__version__ = version("mcghidra")
except Exception:
__version__ = "2025.12.1"
from .server import create_server, main
__all__ = ["create_server", "main", "__version__"]

View File

@ -1,9 +0,0 @@
"""MCGhidra package entry point.
Allows running with: python -m mcghidra
"""
from .server import main
if __name__ == "__main__":
main()

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Test script for the comment functionality in MCGhidra.
Test script for the comment functionality in GhydraMCP.
Tests both HTTP API and MCP bridge interfaces for setting and retrieving
different types of comments in Ghidra, including plate, pre, post, EOL,

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Comprehensive test script for data operations in MCGhidra.
Comprehensive test script for data operations in GhydraMCP.
This script tests all data-related operations including:
1. Creating data items with different types

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Test script for the MCGhidra HTTP API.
Test script for the GhydraMCP HTTP API.
This script tests the HTTP endpoints of the Java plugin.
"""
import json
@ -14,9 +14,9 @@ import sys
DEFAULT_PORT = 8192
# Get host from environment variable or default to localhost
MCGHIDRA_TEST_HOST = os.getenv('MCGHIDRA_TEST_HOST')
if MCGHIDRA_TEST_HOST and MCGHIDRA_TEST_HOST.strip():
BASE_URL = f"http://{MCGHIDRA_TEST_HOST}:{DEFAULT_PORT}"
GHYDRAMCP_TEST_HOST = os.getenv('GHYDRAMCP_TEST_HOST')
if GHYDRAMCP_TEST_HOST and GHYDRAMCP_TEST_HOST.strip():
BASE_URL = f"http://{GHYDRAMCP_TEST_HOST}:{DEFAULT_PORT}"
else:
BASE_URL = f"http://localhost:{DEFAULT_PORT}"
@ -48,8 +48,8 @@ Endpoints requiring HATEOAS updates:
This test suite enforces strict HATEOAS compliance with no backward compatibility.
"""
class MCGhidraHttpApiTests(unittest.TestCase):
"""Test cases for the MCGhidra HTTP API"""
class GhydraMCPHttpApiTests(unittest.TestCase):
"""Test cases for the GhydraMCP HTTP API"""
def assertStandardSuccessResponse(self, data):
"""Helper to assert the standard success response structure for HATEOAS API."""

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Test script for the MCGhidra bridge using the MCP client.
Test script for the GhydraMCP bridge using the MCP client.
This script tests the bridge by sending MCP requests and handling responses.
"""
import json
@ -14,8 +14,8 @@ from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
# Get host and port from environment variables or use defaults
MCGHIDRA_TEST_HOST = os.getenv('MCGHIDRA_TEST_HOST', 'localhost')
MCGHIDRA_TEST_PORT = int(os.getenv('MCGHIDRA_TEST_PORT', '8192'))
GHYDRAMCP_TEST_HOST = os.getenv('GHYDRAMCP_TEST_HOST', 'localhost')
GHYDRAMCP_TEST_PORT = int(os.getenv('GHYDRAMCP_TEST_PORT', '8192'))
# Set up logging
logging.basicConfig(level=logging.INFO)
@ -95,8 +95,8 @@ async def test_bridge():
logger.info(f"List instances result: {list_instances_result}")
# Set the current instance to use for subsequent calls
logger.info(f"Setting current instance to port {MCGHIDRA_TEST_PORT}...")
use_instance_result = await session.call_tool("instances_use", arguments={"port": MCGHIDRA_TEST_PORT})
logger.info(f"Setting current instance to port {GHYDRAMCP_TEST_PORT}...")
use_instance_result = await session.call_tool("instances_use", arguments={"port": GHYDRAMCP_TEST_PORT})
logger.info(f"Use instance result: {use_instance_result}")
# Call the functions_list tool (no port needed now)

38
uv.lock generated
View File

@ -414,6 +414,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/ee/327a3f6c7ac5cde56c7c9449dbc6c0ab78b15c06a51ad5645ab880240120/fastmcp_feedback-2026.1.12.1-py3-none-any.whl", hash = "sha256:6a3dec71f3d3eae4eb0102eb0a86aa7853fb0419fb506a5a13d17deaf842c53c", size = 29789, upload-time = "2026-01-16T02:09:11.831Z" },
]
[[package]]
name = "ghydramcp"
version = "2025.12.3"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
{ name = "fastmcp-feedback" },
{ name = "mcp" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "fastmcp-feedback", specifier = ">=1.0.0" },
{ name = "mcp", specifier = ">=1.22.0" },
{ name = "requests", specifier = ">=2.32.3" },
]
[[package]]
name = "greenlet"
version = "3.3.1"
@ -570,25 +589,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mcghidra"
version = "2026.3.6.1"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
{ name = "fastmcp-feedback" },
{ name = "mcp" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "fastmcp-feedback", specifier = ">=1.0.0" },
{ name = "mcp", specifier = ">=1.22.0" },
{ name = "requests", specifier = ">=2.32.3" },
]
[[package]]
name = "mcp"
version = "1.23.1"