Compare commits
No commits in common. "main" and "feat/api-gap-fill" have entirely different histories.
main
...
feat/api-g
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@ -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
|
||||
@ -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!
|
||||
@ -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",
|
||||
|
||||
36
Makefile
36
Makefile
@ -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
|
||||
|
||||
@ -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
753
README.md
@ -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... │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
[](https://github.com/starsong-consulting/GhydraMCP/releases)
|
||||
[](https://github.com/starsong-consulting/GhydraMCP/blob/main/GHIDRA_HTTP_API.md)
|
||||
[](https://github.com/starsong-consulting/GhydraMCP/stargazers)
|
||||
[](https://github.com/starsong-consulting/GhydraMCP/network/members)
|
||||
[](https://github.com/starsong-consulting/GhydraMCP/graphs/contributors)
|
||||
[](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 |
|
||||

|
||||
|
||||
**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
|
||||
|
||||
- **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
|
||||
|
||||
- **Memory Operations**:
|
||||
- Direct memory reading with hex and raw byte representation
|
||||
- Address space navigation and mapping
|
||||
- Memory segment analysis
|
||||
|
||||
Claude will auto-start a container, wait for analysis, and begin work.
|
||||
- **Symbol Management**:
|
||||
- View and analyze imports and exports
|
||||
- Identify library functions and dependencies
|
||||
- Symbol table exploration and manipulation
|
||||
- Namespace hierarchy visualization
|
||||
|
||||
### Option 2: Native Ghidra
|
||||
## Interactive Reverse Engineering
|
||||
|
||||
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`
|
||||
- **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
|
||||
|
||||
2. **Add MCP server:**
|
||||
```bash
|
||||
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
|
||||
```
|
||||
## Complete Data Manipulation
|
||||
|
||||
3. **Open a binary in Ghidra**, then ask Claude to analyze it.
|
||||
- **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
|
||||
|
||||
---
|
||||
- **Function Manipulation**:
|
||||
- Rename functions with proper scoping
|
||||
- Update function signatures with parameter information
|
||||
- Modify local variable names and types
|
||||
- Set function return types
|
||||
|
||||
## How It Works
|
||||
## Multi-instance Support
|
||||
|
||||
```
|
||||
┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
|
||||
│ Claude │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
|
||||
│ (or other │ stdio │ (Python) │ REST API │ Plugin │
|
||||
│ MCP client) │ │ │ │ (Java) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
- 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
|
||||
|
||||
- **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
|
||||
## 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
|
||||
|
||||
## Usage Patterns
|
||||
# Installation
|
||||
|
||||
### Set Current Instance (Then Forget About Ports)
|
||||
## 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
|
||||
|
||||
14
TESTING.md
14
TESTING.md
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}/ || exit 1
|
||||
CMD curl -f http://localhost:${GHYDRA_PORT}/ || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@ -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,30 +412,14 @@ 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
|
||||
pass
|
||||
|
||||
def _send_response(self, exchange, code, data):
|
||||
response_bytes = json.dumps(data, indent=2).encode('utf-8')
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,25 +28,25 @@ 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 ""
|
||||
echo "Starting in wait mode..."
|
||||
@ -78,7 +78,7 @@ case "${MCGHIDRA_MODE}" in
|
||||
-import "${BINARY_PATH}"
|
||||
-max-cpu 2
|
||||
-scriptPath "${SCRIPT_DIR}"
|
||||
-postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}"
|
||||
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}"
|
||||
)
|
||||
|
||||
# Add any extra arguments passed
|
||||
@ -93,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
|
||||
@ -110,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
|
||||
|
||||
@ -138,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
|
||||
;;
|
||||
|
||||
20
pom.xml
20
pom.xml
@ -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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcghidra"
|
||||
version = "2026.2.11"
|
||||
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"
|
||||
@ -15,14 +15,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mcghidra = "mcghidra:main"
|
||||
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"]
|
||||
|
||||
10
run_tests.py
10
run_tests.py
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""MCGhidra - AI-assisted reverse engineering bridge for Ghidra.
|
||||
"""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.
|
||||
@ -6,7 +6,7 @@ Python bridge for decompilation, analysis & binary manipulation.
|
||||
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
__version__ = version("mcghidra")
|
||||
__version__ = version("ghydramcp")
|
||||
except Exception:
|
||||
__version__ = "2025.12.1"
|
||||
|
||||
9
src/ghydramcp/__main__.py
Normal file
9
src/ghydramcp/__main__.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""GhydraMCP package entry point.
|
||||
|
||||
Allows running with: python -m ghydramcp
|
||||
"""
|
||||
|
||||
from .server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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
|
||||
@ -1,4 +1,4 @@
|
||||
"""Core infrastructure for MCGhidra.
|
||||
"""Core infrastructure for GhydraMCP.
|
||||
|
||||
Contains HTTP client, pagination, progress reporting, and logging utilities.
|
||||
"""
|
||||
@ -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."""
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -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]
|
||||
|
||||
@ -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",
|
||||
@ -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
|
||||
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"),
|
||||
)
|
||||
try:
|
||||
payload = {"comment": comment}
|
||||
response = self.safe_patch(port, f"functions/{address}", payload)
|
||||
if response.get("success", False):
|
||||
return self.simplify_response(response)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to pre-comment
|
||||
return self.comments_set(
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
@ -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.
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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.
|
||||
@ -19,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 mcghidra.core.logging import logger
|
||||
from mcghidra.mixins.base import MCGhidraMixinBase
|
||||
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
|
||||
|
||||
# Port pool configuration (32 ports should handle many concurrent sessions)
|
||||
PORT_POOL_START = 8192
|
||||
PORT_POOL_END = 8223
|
||||
PORT_LOCK_DIR = Path("/tmp/mcghidra-ports")
|
||||
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
|
||||
@ -66,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
|
||||
@ -81,20 +77,14 @@ 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({
|
||||
"session_id": session_id,
|
||||
"pid": os.getpid(),
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
os.write(fd, lock_data.encode())
|
||||
except Exception:
|
||||
# Write failed - release the lock
|
||||
os.close(fd)
|
||||
raise
|
||||
os.ftruncate(fd, 0)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
lock_data = json.dumps({
|
||||
"session_id": session_id,
|
||||
"pid": os.getpid(),
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
os.write(fd, lock_data.encode())
|
||||
|
||||
# Keep the file descriptor open to maintain the lock
|
||||
# Store it so we can release later
|
||||
@ -104,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]:
|
||||
@ -145,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]]:
|
||||
@ -203,17 +191,17 @@ 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 (8192-8223)
|
||||
@ -231,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
|
||||
@ -248,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')
|
||||
@ -284,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],
|
||||
@ -331,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,
|
||||
@ -345,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
|
||||
@ -356,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.
|
||||
@ -378,12 +336,12 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
f"{self.LABEL_PREFIX}.pid": str(os.getpid()),
|
||||
}
|
||||
|
||||
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")
|
||||
@ -401,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,
|
||||
@ -431,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,
|
||||
@ -467,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}}",
|
||||
]
|
||||
@ -517,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}}",
|
||||
]
|
||||
@ -546,7 +505,7 @@ 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,
|
||||
@ -555,9 +514,9 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
name: 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.
|
||||
|
||||
@ -581,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)
|
||||
@ -590,42 +557,23 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
|
||||
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
|
||||
# This handles external containers (not managed by MCGhidra) using ports in our range
|
||||
port = None
|
||||
ports_tried = []
|
||||
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
|
||||
)
|
||||
if port_check.stdout.strip():
|
||||
# Port is in use by Docker - release and try next
|
||||
ports_tried.append(candidate_port)
|
||||
self.port_pool.release(candidate_port)
|
||||
continue
|
||||
|
||||
# Found a usable port!
|
||||
port = candidate_port
|
||||
break
|
||||
|
||||
if port is None:
|
||||
# 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():
|
||||
self._port_pool.release(port)
|
||||
return {
|
||||
"error": "Port pool exhausted (8192-8223). All ports are in use by Docker containers.",
|
||||
"ports_checked": ports_tried if ports_tried else "all ports locked by other MCGhidra sessions",
|
||||
"allocated_ports": self.port_pool.get_allocated_ports(),
|
||||
"error": f"Port {port} is already in use by another container"
|
||||
}
|
||||
|
||||
# Build label arguments
|
||||
@ -635,7 +583,7 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
label_args.extend(["-l", f"{k}={v}"])
|
||||
|
||||
# Start the container
|
||||
run_result = await self._run_docker_cmd(
|
||||
run_result = self._run_docker_cmd(
|
||||
[
|
||||
"run",
|
||||
"-d",
|
||||
@ -646,9 +594,9 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
"-v",
|
||||
f"{binary_file.parent}:/binaries:ro",
|
||||
"-e",
|
||||
f"MCGHIDRA_MAXMEM={memory}",
|
||||
f"GHYDRA_MAXMEM={memory}",
|
||||
*label_args,
|
||||
"mcghidra:latest",
|
||||
"ghydramcp:latest",
|
||||
f"/binaries/{binary_file.name}",
|
||||
]
|
||||
)
|
||||
@ -679,17 +627,17 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
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
|
||||
@ -709,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",
|
||||
@ -735,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 = {
|
||||
@ -763,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,
|
||||
@ -772,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
|
||||
@ -791,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,
|
||||
@ -803,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,
|
||||
@ -812,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
|
||||
@ -835,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"
|
||||
@ -846,7 +794,7 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
args = [
|
||||
"build",
|
||||
"-t",
|
||||
f"mcghidra:{tag}",
|
||||
f"ghydramcp:{tag}",
|
||||
"-f",
|
||||
str(dockerfile),
|
||||
]
|
||||
@ -855,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,
|
||||
}
|
||||
|
||||
@ -910,32 +858,75 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
|
||||
@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,
|
||||
wait: bool = False,
|
||||
timeout: float = 300.0,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Automatically start a Docker container with intelligent port allocation.
|
||||
@ -943,20 +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 (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
|
||||
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
|
||||
|
||||
@ -989,10 +979,10 @@ 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."
|
||||
)
|
||||
}
|
||||
@ -1007,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,
|
||||
@ -1014,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(
|
||||
@ -1028,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)
|
||||
@ -1056,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,
|
||||
)
|
||||
@ -1091,8 +1106,9 @@ class DockerMixin(MCGhidraMixinBase):
|
||||
pass
|
||||
|
||||
# Clean up stale port locks
|
||||
stale_ports = self.port_pool.cleanup_stale_locks(max_age_hours * 3600)
|
||||
result["ports_cleaned"] = stale_ports
|
||||
if self._port_pool:
|
||||
stale_ports = self._port_pool.cleanup_stale_locks(max_age_hours * 3600)
|
||||
result["ports_cleaned"] = stale_ports
|
||||
|
||||
return result
|
||||
|
||||
@ -1116,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}",
|
||||
}
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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)
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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:
|
||||
@ -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,27 +148,21 @@ 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(f"🔬 GhydraMCP v{package_version}", file=sys.stderr)
|
||||
print(" AI-assisted reverse engineering bridge for Ghidra", file=sys.stderr)
|
||||
|
||||
# Check Docker availability
|
||||
@ -191,20 +184,19 @@ 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] = {
|
||||
"url": f"http://{config.ghidra_host}:{port}",
|
||||
"project": response.get("project", ""),
|
||||
"file": response.get("file", ""),
|
||||
"discovered_at": time.time(),
|
||||
}
|
||||
GhydraMixinBase._instances[port] = {
|
||||
"url": f"http://{config.ghidra_host}:{port}",
|
||||
"project": response.get("project", ""),
|
||||
"file": response.get("file", ""),
|
||||
"discovered_at": time.time(),
|
||||
}
|
||||
found += 1
|
||||
print(f" ✓ Found instance on port {port}", file=sys.stderr)
|
||||
except Exception:
|
||||
@ -219,7 +211,7 @@ def main():
|
||||
discovery_thread = threading.Thread(
|
||||
target=_periodic_discovery,
|
||||
daemon=True,
|
||||
name="MCGhidra-Discovery",
|
||||
name="GhydraMCP-Discovery",
|
||||
)
|
||||
discovery_thread.start()
|
||||
|
||||
@ -39,14 +39,14 @@ import ghidra.util.Msg;
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ghidra.app.DeveloperPluginPackage.NAME,
|
||||
category = PluginCategoryNames.ANALYSIS,
|
||||
shortDescription = "MCGhidra Plugin for AI Analysis",
|
||||
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;
|
||||
@ -54,10 +54,10 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
private boolean isBaseInstance = false;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@ -70,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();
|
||||
@ -111,9 +111,9 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -350,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);
|
||||
|
||||
@ -449,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();
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
"""MCGhidra package entry point.
|
||||
|
||||
Allows running with: python -m mcghidra
|
||||
"""
|
||||
|
||||
from .server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
38
uv.lock
generated
@ -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.2.11"
|
||||
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user