Compare commits

...

13 Commits

Author SHA1 Message Date
112c1969c8 Fix port allocation to skip ports used by external Docker containers
Some checks failed
Build Ghidra Plugin / build (push) Has been cancelled
When port 8192 was already in use by a non-MCGhidra container (e.g.,
LTspice), docker_start would fail instead of trying the next port.
Now loops through the pool, checking each candidate against Docker's
published ports before using it.

Also includes Docker build retry improvements from earlier session.
2026-02-11 05:37:40 -07:00
57f042a802 Fix exception handling for functions_create and data_create
- Change from 'except Exception' to bare 'except' to catch Java
  exceptions from Ghidra that don't inherit from Python Exception
- Use sys.exc_info() to safely extract error messages when str(e)
  might fail on certain Java exception types
- Add null checks after getAddress() since it can return None
  instead of throwing for invalid addresses
- Add last-resort response handling to prevent silent connection
  drops when exception handling itself fails

These endpoints now return proper JSON error responses instead of
causing "Empty reply from server" errors.
2026-02-07 06:22:25 -07:00
842035ca92 Remove dead UI tools that can never work in headless MCP mode
ui_get_current_address and ui_get_current_function require Ghidra GUI
context to know what the user has selected. Since MCP always runs
headless (Docker container), these tools always fail with HEADLESS_MODE
error. Removed them to avoid confusion.

Alternative: Use explicit addresses with functions_get(address=...) or
data_list(addr=...) instead.
2026-02-07 06:01:30 -07:00
c930e7c059 fix: Complete rename of remaining ghydra references
Some checks failed
Build Ghidra Plugin / build (push) Has been cancelled
- Rename docker/GhydraMCPServer.py → MCGhidraServer.py
- Update extension.properties, MANIFEST.MF, Module.manifest
- Update .env and .env.example env var names
2026-02-07 02:28:54 -07:00
1143489924 refactor: Rename project from ghydramcp to mcghidra
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
- Rename src/ghydramcp → src/mcghidra
- Rename GhydraMCPPlugin.java → MCGhidraPlugin.java
- Update all imports, class names, and references
- Update pyproject.toml package name and script entry
- Update Docker image names and container prefixes
- Update environment variables: GHYDRA_* → MCGHIDRA_*
- Update all documentation references
2026-02-07 02:13:53 -07:00
d1750cb339 fix: Address code review issues across core modules
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
- http_client: Defensive copy before .pop() to avoid mutating caller's dict
- analysis.py: Add debug logging for fallback paths instead of silent swallow
- docker.py: Add debug logging to PortPool exception handlers
- docker.py: Fix file descriptor leak in _try_acquire_port with inner try/except
- docker.py: Lazy PortPool initialization via property to avoid side effects
- server.py: Wrap initial discovery in _instances_lock for thread safety
- server.py: Call configure_logging() at startup with GHYDRAMCP_DEBUG support
- pagination.py: Use SHA-256 instead of MD5 for query hash consistency
- base.py: Add proper type annotations (Dict[str, Any])
- filtering.py: Use List[str] from typing for consistency
- filtering.py: Add docstrings to private helper methods
- structs.py: Rename project_fields param to fields for API consistency
- logging.py: Fix import path from deprecated mcp.server.fastmcp to fastmcp
2026-02-06 04:50:47 -07:00
04f3011413 docs: Rewrite README for clarity and current features
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
- Punchy hero section with terminal-style demo
- Feature table showing all 64 tools across 12 categories
- Docker quickstart as primary path (easiest)
- Clear usage patterns: current instance, Docker workflow, pagination
- Concise tool reference grouped by category
- Architecture notes explaining AI-agent design decisions
- Removed outdated v2.1 references and verbose API examples
2026-02-06 00:58:52 -07:00
41bd8445e9 fix: Make docker_health use current instance port by default
docker_health now uses get_instance_port(port) like all other tools,
so it defaults to the current working instance when no port is specified.

Workflow:
1. docker_auto_start(binary) -> returns port
2. Poll docker_health(port=N) until healthy
3. instances_use(port=N) to set as current
4. All subsequent analysis calls omit port
2026-02-06 00:49:41 -07:00
d298a89f5f refactor: Remove docker_wait tool entirely
docker_wait was the same anti-pattern as wait param - it blocked
a single tool call for up to 5 minutes with no visibility.

LLMs should poll docker_health(port) in their own loop. This gives:
- Visibility into progress between polls
- Ability to check docker_logs while waiting
- Control over timeout and retry logic
- Opportunity to bail out early
2026-02-06 00:48:26 -07:00
5300fb24b8 refactor: Remove wait/timeout params from docker_auto_start
The wait parameter was a convenience anti-pattern that caused LLMs
to block on a single tool call for up to 5 minutes with no visibility
into progress.

Now docker_auto_start always returns immediately. Clients should use
docker_wait(port) separately to poll for container readiness. This
gives visibility into progress and allows early bailout.
2026-02-06 00:44:44 -07:00
6662c8411a fix: Make all Docker subprocess calls non-blocking
Previously only docker_health was fixed to use run_in_executor(),
but all other Docker operations (docker_status, docker_start,
docker_stop, docker_logs, docker_build, docker_cleanup) still
used synchronous subprocess.run() which blocked the async event
loop. This caused docker_auto_start(wait=True) to freeze the
entire MCP server.

Now _run_docker_cmd is async and runs subprocess calls in thread
executor. All callers updated to use await.
2026-02-06 00:41:25 -07:00
f1986db6cc docs: Update CHANGELOG with Sprint 3+4 features and stability fixes
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
Added:
- Symbol CRUD operations (create/rename/delete/imports/exports)
- Bookmark management tools
- Enum and typedef creation
- Variable management (list/rename/functions_variables)
- Namespace and class tools
- Memory segment tools

Changed:
- Docker port allocation now auto-allocated from pool (8192-8223)
- docker_auto_start defaults to wait=False

Fixed:
- instances_use hanging (lazy registration pattern)
- Event loop blocking in docker_health (run_in_executor)
- Session isolation for docker_stop/docker_cleanup
- Background discovery thread timeout (30s → 0.5s)
- Typedef/variable type resolution
2026-02-05 10:39:18 -07:00
7eefdda9f8 Merge feat/api-gap-fill: Session isolation, non-blocking I/O, CRUD operations
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
Sprint 3+4 API gap filling plus critical stability fixes:

Features:
- Symbol CRUD (create, rename, delete)
- Bookmark management (list, create, delete)
- Enum/typedef creation
- Variable rename with type resolution

Stability fixes:
- Lazy instances_use (no blocking HTTP calls)
- Non-blocking health checks via thread executor
- Session isolation for docker_stop/cleanup
- Auto port allocation (removed client-specified ports)
- wait=False default for docker_auto_start
2026-02-05 09:27:26 -07:00
57 changed files with 980 additions and 1168 deletions

View File

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

View File

@ -7,6 +7,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] ## [Unreleased]
### Added ### 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: - **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 - `malware_triage` - Reports progress across 21 scanning steps
- `analyze_imports` - Reports progress across 12 capability categories - `analyze_imports` - Reports progress across 12 capability categories
@ -33,6 +57,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `document_struct` - Comprehensively document data structure fields and usage - `document_struct` - Comprehensively document data structure fields and usage
- `find_error_handlers` - Map error handling, cleanup routines, and exit paths - `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 ## [2025.12.1] - 2025-12-01
### Added ### Added
@ -188,7 +239,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [1.1] - 2025-03-30 ## [1.1] - 2025-03-30
### Added ### Added
- Initial release of GhydraMCP bridge - Initial release of MCGhidra bridge
- Basic Ghidra instance management tools - Basic Ghidra instance management tools
- Function analysis tools - Function analysis tools
- Variable manipulation tools - Variable manipulation tools
@ -199,11 +250,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Initial project setup - Initial project setup
- Basic MCP bridge functionality - Basic MCP bridge functionality
[unreleased]: https://github.com/teal-bauer/GhydraMCP/compare/v2025.12.1...HEAD [unreleased]: https://github.com/teal-bauer/MCGhidra/compare/v2025.12.1...HEAD
[2025.12.1]: https://github.com/teal-bauer/GhydraMCP/compare/v2.0.0...v2025.12.1 [2025.12.1]: https://github.com/teal-bauer/MCGhidra/compare/v2.0.0...v2025.12.1
[2.0.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.4.0...v2.0.0 [2.0.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.4.0...v2.0.0
[1.4.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.3.0...v1.4.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/GhydraMCP/compare/v1.2...v1.3.0 [1.3.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.2...v1.3.0
[1.2]: https://github.com/teal-bauer/GhydraMCP/compare/v1.1...v1.2 [1.2]: https://github.com/teal-bauer/MCGhidra/compare/v1.1...v1.2
[1.1]: https://github.com/teal-bauer/GhydraMCP/compare/1.0...v1.1 [1.1]: https://github.com/teal-bauer/MCGhidra/compare/1.0...v1.1
[1.0]: https://github.com/teal-bauer/GhydraMCP/releases/tag/1.0 [1.0]: https://github.com/teal-bauer/MCGhidra/releases/tag/1.0

View File

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

View File

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

View File

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

View File

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

753
README.md
View File

@ -1,601 +1,302 @@
# MCGhidra
**AI-native reverse engineering.** Give Claude (or any MCP client) direct access to Ghidra's analysis engine.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ "Analyze the authentication bypass in this firmware" │
│ │
│ Claude: I'll decompile the auth functions and trace the validation logic. │
│ │
│ [functions_list grep="auth|login|verify"] │
│ [functions_decompile name="verify_password"] │
│ [xrefs_list to_addr="0x0040156c"] │
│ [analysis_get_dataflow address="0x00401234" direction="backward"] │
│ │
│ Found it. The password check at 0x401580 compares against a hardcoded │
│ hash, but there's a debug backdoor at 0x401590 that bypasses validation │
│ when the username starts with "debug_". Let me show you the call graph... │
└─────────────────────────────────────────────────────────────────────────────┘
```
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/releases)
[![API Version](https://img.shields.io/badge/API-v2.1-orange)](https://github.com/starsong-consulting/GhydraMCP/blob/main/GHIDRA_HTTP_API.md)
[![GitHub stars](https://img.shields.io/github/stars/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/network/members)
[![GitHub contributors](https://img.shields.io/github/contributors/starsong-consulting/GhydraMCP)](https://github.com/starsong-consulting/GhydraMCP/graphs/contributors)
[![Build Status](https://github.com/starsong-consulting/GhydraMCP/actions/workflows/build.yml/badge.svg)](https://github.com/starsong-consulting/GhydraMCP/actions/workflows/build.yml)
# GhydraMCP v2.1 ## What You Get
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). **64 MCP tools** across 12 categories:
![GhydraMCP logo](https://github.com/user-attachments/assets/86b9b2de-767c-4ed5-b082-510b8109f00f) | 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 |
## Overview **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...
GhydraMCP v2.1 integrates three key components: **11 MCP resources** for quick enumeration without tool calls.
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
This architecture enables AI assistants like Claude to seamlessly: ## Quick Start
- 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
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. ### Option 1: Docker (Easiest)
# Features No Ghidra installation needed. Analyze binaries in isolated containers.
GhydraMCP version 2.1 provides a comprehensive set of reverse engineering capabilities to AI assistants through its HATEOAS-driven API: ```bash
# Build the image (once)
cd MCGhidra && docker build -t mcghidra:latest -f docker/Dockerfile .
## Advanced Program Analysis # Add to your MCP config
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
```
- **Enhanced Decompilation**: Then in Claude:
- Convert binary functions to readable C code ```
- Toggle between clean C-like pseudocode and raw decompiler output Analyze /path/to/suspicious.exe
- Show/hide syntax trees for detailed analysis ```
- Multiple simplification styles for different analysis approaches
- **Comprehensive Static Analysis**: Claude will auto-start a container, wait for analysis, and begin work.
- 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**: ### Option 2: Native Ghidra
- Direct memory reading with hex and raw byte representation
- Address space navigation and mapping
- Memory segment analysis
- **Symbol Management**: 1. **Install the Ghidra plugin:**
- View and analyze imports and exports - Download latest [release](https://github.com/starsong-consulting/MCGhidra/releases)
- Identify library functions and dependencies - In Ghidra: `File → Install Extensions → +` → select the `.zip`
- Symbol table exploration and manipulation - Restart Ghidra
- Namespace hierarchy visualization - Enable in `File → Configure → Developer → MCGhidraPlugin`
## Interactive Reverse Engineering 2. **Add MCP server:**
```bash
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
```
- **Code Understanding**: 3. **Open a binary in Ghidra**, then ask Claude to analyze it.
- 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
## Complete Data Manipulation ## How It Works
- **Data Creation and Management**: ```
- Create new data items with specified types ┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
- Delete existing data items │ Claude │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
- Rename data items with proper scope handling │ (or other │ stdio │ (Python) │ REST API │ Plugin │
- Set and update data types for existing items │ MCP client) │ │ │ │ (Java) │
- Combined rename and retype operations └──────────────┘ └──────────────┘ └──────────────┘
- Type definition management ```
- **Function Manipulation**: - **Ghidra Plugin**: Exposes Ghidra's analysis via HTTP REST API (HATEOAS)
- Rename functions with proper scoping - **MCGhidra Server**: Translates MCP tool calls to API requests
- Update function signatures with parameter information - **Multi-instance**: Analyze multiple binaries simultaneously on different ports
- Modify local variable names and types - **Session isolation**: Docker containers get unique ports, preventing conflicts
- Set function return types
## Multi-instance Support ---
- Run multiple Ghidra instances simultaneously ## Usage Patterns
- 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
## Program Navigation and Discovery ### Set Current Instance (Then Forget About Ports)
- List and search functions, classes, and namespaces
- View memory segments and layout
- Search by name, pattern, or signature
- Resource discovery through HATEOAS links
- Pagination for handling large result sets
- Filtering capabilities across all resources
# Installation
## Prerequisites
- Install [Ghidra](https://ghidra-sre.org)
- Python3
- MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
## Ghidra
First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/releases) from this repository. The "Complete" artifact contains the zipped Ghidra plugin and the Python MCP bridge. Unpack the outer archive, then, add the plugin to Ghidra:
1. Run Ghidra
2. Select `File` -> `Install Extensions`
3. Click the `+` button
4. Select the `GhydraMCP-[version].zip` file from the downloaded release
5. Restart Ghidra
6. Make sure the GhydraMCPPlugin is enabled in `File` -> `Configure` -> `Developer`
> **Note:** By default, the first CodeBrowser opened in Ghidra gets port 8192, the second gets 8193, and so on. You can check which ports are being used by looking at the Console in the Ghidra main (project) window - click the computer icon in the bottom right to "Open Console". Look for log entries like:
> ```
> (HydraMCPPlugin) Plugin loaded on port 8193
> (HydraMCPPlugin) HydraMCP HTTP server started on port 8193
> ```
>
> GhydraMCP now includes auto-discovery of running Ghidra instances, so manually registering each instance is typically not necessary. The MCP bridge will automatically discover and register instances on startup and periodically check for new ones.
Video Installation Guide:
https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3
## MCP Clients
GhydraMCP works with any MCP-compatible client using **stdio transport**. It has been tested and confirmed working with:
- **Claude Desktop** - Anthropic's official desktop application
- **Claude Code** - Anthropic's VS Code extension and CLI tool
- **Cline** - Popular VS Code extension for AI-assisted coding
See the [Client Setup](#client-setup) section below for detailed configuration instructions for each client.
## API Reference (Updated for v2.1)
### Available Tools
GhydraMCP v2.1 organizes tools into logical namespaces for better discoverability and organization:
**Instance Management** (`instances_*`):
- `instances_list`: List active Ghidra instances (auto-discovers on default host) - **use this first**
- `instances_discover`: Discover instances on a specific host (params: host [optional]) - **only use for non-default hosts**
- `instances_register`: Register new instance (params: port, url [optional])
- `instances_unregister`: Remove instance (params: port)
- `instances_use`: Set current working instance (params: port)
- `instances_current`: Get current working instance info
**Function Analysis** (`functions_*`):
- `functions_list`: List all functions (params: offset, limit, port [optional])
- `functions_get`: Get function details (params: name or address, port [optional])
- `functions_decompile`: Get decompiled C code (params: name or address, syntax_tree, style, timeout, port [optional])
- `functions_disassemble`: Get disassembled instructions (params: name or address, port [optional])
- `functions_create`: Create function at address (params: address, port [optional])
- `functions_rename`: Rename a function (params: old_name or address, new_name, port [optional])
- `functions_set_signature`: Update function prototype (params: name or address, signature, port [optional])
- `functions_get_variables`: Get function variables (params: name or address, port [optional])
- `functions_set_comment`: Set function comment (params: address, comment, port [optional])
**Data Manipulation** (`data_*`):
- `data_list`: List data items (params: offset, limit, addr, name, name_contains, port [optional])
- `data_list_strings`: List all defined strings (params: offset, limit, filter, port [optional])
- `data_create`: Create data at address (params: address, data_type, size [optional], port [optional])
- `data_rename`: Rename data item (params: address, name, port [optional])
- `data_delete`: Delete data item (params: address, port [optional])
- `data_set_type`: Change data type (params: address, data_type, port [optional])
**Struct Management** (`structs_*`):
- `structs_list`: List all struct data types (params: offset, limit, category [optional], port [optional])
- `structs_get`: Get detailed struct information (params: name, port [optional])
- `structs_create`: Create new struct (params: name, category [optional], description [optional], port [optional])
- `structs_add_field`: Add field to struct (params: struct_name, field_name, field_type, offset [optional], comment [optional], port [optional])
- `structs_update_field`: Update struct field (params: struct_name, field_name or field_offset, new_name [optional], new_type [optional], new_comment [optional], port [optional])
- `structs_delete`: Delete struct (params: name, port [optional])
**Memory Operations** (`memory_*`):
- `memory_read`: Read bytes from memory (params: address, length, format, port [optional])
- `memory_write`: Write bytes to memory (params: address, bytes_data, format, port [optional])
**Cross-References** (`xrefs_*`):
- `xrefs_list`: List cross-references (params: to_addr [optional], from_addr [optional], type [optional], offset, limit, port [optional])
**Analysis** (`analysis_*`):
- `analysis_run`: Trigger program analysis (params: port [optional], analysis_options [optional])
- `analysis_get_callgraph`: Get function call graph (params: name or address, max_depth, port [optional])
- `analysis_get_dataflow`: Perform data flow analysis (params: address, direction, max_steps, port [optional])
**Example Usage**:
```python ```python
# Instance Management - Always start here instances_list() # Discover running Ghidra instances
client.use_tool("ghydra", "instances_list") # Auto-discovers instances on localhost instances_use(port=8192) # Set as current
client.use_tool("ghydra", "instances_use", {"port": 8192}) # Set working instance functions_list() # No port needed!
client.use_tool("ghydra", "instances_current") # Check current instance data_list_strings(grep="password") # Uses 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
``` ```
## Client Setup ### Docker Workflow
GhydraMCP works with any MCP-compatible client. Below are configuration examples for popular AI coding assistants. ```python
# Start container (returns immediately)
result = docker_auto_start(binary_path="/path/to/malware.exe")
# → {port: 8195, message: "Poll docker_health(port=8195)..."}
### Installation Methods # Poll until ready
while True:
health = docker_health(port=8195)
if health["healthy"]:
break
# Can check docker_logs() while waiting
#### Recommended: Local Installation from Release # Register and use
instances_use(port=8195)
functions_list() # Ready to analyze
```
Download the latest [release](https://github.com/starsong-consulting/GhydraMCP/releases) to ensure the bridge and plugin versions are in sync. ### Cursor-Based Pagination
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 ```json
{ {
"mcpServers": { "mcpServers": {
"ghydra": { "mcghidra": {
"command": "uv", "command": "uv",
"args": [ "args": ["run", "--directory", "/path/to/MCGhidra", "mcghidra"]
"run",
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
}
}
}
}
```
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.
### Claude Desktop Configuration
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
} }
} }
} }
``` ```
If you prefer to use `python` directly instead of `uv`: **Claude Code**:
```bash
```json claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
{
"mcpServers": {
"ghydra": {
"command": "python",
"args": [
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
],
"env": {
"GHIDRA_HYDRA_HOST": "localhost"
},
"disabled": false
}
}
}
``` ```
> **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. ## Tool Reference
### 5ire Configuration ### Instance Management
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:**
``` ```
Analyze the main function in the binary and identify any data structures being used. instances_list # Discover Ghidra instances (use this first!)
instances_use # Set current working instance
instances_current # Show current instance info
``` ```
**Assistant:** ### Function Analysis
``` ```
I'll analyze the binary for you. Let me start by checking available Ghidra instances. functions_list # List functions (supports grep, pagination)
functions_get # Get function details by name or address
[Uses instances_list] functions_decompile # Decompile to C pseudocode
Found Ghidra instance on port 8192 analyzing "malware_sample.exe". functions_disassemble # Get assembly instructions
functions_rename # Rename a function
[Uses instances_use with port 8192] functions_set_signature # Set function prototype
Set instance 8192 as current working instance. functions_set_comment # Add decompiler comment
functions_create # Create function at address
[Uses functions_decompile with name "main"] functions_variables # List local variables and parameters
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.
``` ```
**User:** ### Data Operations
``` ```
Can you trace where process_packet is called from and show me the call graph? 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
``` ```
**Assistant:** ### Cross-References & Analysis
``` ```
I'll trace the call relationships for process_packet. xrefs_list # Find cross-references to/from address
analysis_get_callgraph # Generate call graph
[Uses xrefs_list with to_addr for process_packet] analysis_get_dataflow # Trace data flow forward/backward
Found 3 callers of process_packet: analysis_run # Trigger Ghidra auto-analysis
- 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.
``` ```
# JSON Communication ### Structs & Types
```
GhydraMCP uses structured JSON for all communication between the Python bridge and Java plugin. This ensures consistent and reliable data exchange. structs_list # List struct definitions
structs_get # Get struct with all fields
## API Architecture structs_create # Create new struct
structs_add_field # Add field to struct
GhydraMCP v2.1 implements a comprehensive HATEOAS-driven REST API that follows hypermedia design principles: structs_update_field # Modify existing field
structs_delete # Remove struct
### Core API Design enums_list / enums_create
typedefs_list / typedefs_create
- **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: ### Docker Management
```
```json docker_auto_start # Start container for binary (auto port allocation)
{ docker_health # Check if container API is responding
"id": "req-123", docker_status # List all containers and images
"instance": "http://localhost:8192", docker_start # Manual container start
"success": true, docker_stop # Stop container (session-scoped)
"result": [ ... objects ... ], docker_logs # Get container logs
"size": 150, docker_cleanup # Remove orphaned containers
"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: See `--help` or the [API docs](GHIDRA_HTTP_API.md) for full parameter details.
```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. ## Building from Source
# Testing ```bash
# Clone
git clone https://github.com/starsong-consulting/MCGhidra
cd MCGhidra
GhydraMCP includes comprehensive test suites for both the HTTP API and MCP bridge. See [TESTING.md](TESTING.md) for details on running the tests. # Build Ghidra plugin
## 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 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
## Build Ghidra Plugin Only ## Architecture
If you only need the Ghidra plugin:
``` MCGhidra is designed for AI agents:
mvn clean package -P plugin-only
```
## Build Complete Package Only - **Lazy registration**: `instances_use` doesn't block — validates on first real call
If you only need the combined package: - **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
``` Based on [GhidraMCP by Laurie Wired](https://github.com/LaurieWired/GhidraMCP/), evolved into a comprehensive RE platform.
mvn clean package -P complete-only
```
The Ghidra plugin includes these files required for Ghidra to recognize the extension: ---
- lib/GhydraMCP.jar
- extension.properties ## License
- Module.manifest
Apache 2.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,26 @@
#!/bin/bash #!/bin/bash
# GhydraMCP Docker Entrypoint # MCGhidra Docker Entrypoint
# Starts Ghidra in headless mode with HTTP API server # Starts Ghidra in headless mode with HTTP API server
set -e set -e
GHYDRA_MODE=${GHYDRA_MODE:-headless} MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
GHYDRA_PORT=${GHYDRA_PORT:-8192} MCGHIDRA_PORT=${MCGHIDRA_PORT:-8192}
GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-2G} MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-2G}
GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra} GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra}
# User scripts directory - Python scripts don't need OSGi bundle registration # User scripts directory - Python scripts don't need OSGi bundle registration
SCRIPT_DIR=${SCRIPT_DIR:-/home/ghidra/ghidra_scripts} SCRIPT_DIR=${SCRIPT_DIR:-/home/ghidra/ghidra_scripts}
# Project settings # Project settings
PROJECT_DIR=${PROJECT_DIR:-/projects} PROJECT_DIR=${PROJECT_DIR:-/projects}
PROJECT_NAME=${PROJECT_NAME:-GhydraMCP} PROJECT_NAME=${PROJECT_NAME:-MCGhidra}
echo "==============================================" echo "=============================================="
echo " GhydraMCP Docker Container" echo " MCGhidra Docker Container"
echo "==============================================" echo "=============================================="
echo " Mode: ${GHYDRA_MODE}" echo " Mode: ${MCGHIDRA_MODE}"
echo " Port: ${GHYDRA_PORT}" echo " Port: ${MCGHIDRA_PORT}"
echo " Memory: ${GHYDRA_MAXMEM}" echo " Memory: ${MCGHIDRA_MAXMEM}"
echo " Project: ${PROJECT_DIR}/${PROJECT_NAME}" echo " Project: ${PROJECT_DIR}/${PROJECT_NAME}"
echo "==============================================" echo "=============================================="
@ -28,25 +28,25 @@ echo "=============================================="
mkdir -p "${PROJECT_DIR}" mkdir -p "${PROJECT_DIR}"
# Handle different modes # Handle different modes
case "${GHYDRA_MODE}" in case "${MCGHIDRA_MODE}" in
headless) headless)
# Headless mode: Import a binary and start HTTP server # Headless mode: Import a binary and start HTTP server
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "" echo ""
echo "Usage: docker run ghydramcp:latest [binary_path] [options]" echo "Usage: docker run mcghidra:latest [binary_path] [options]"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " # Analyze a binary mounted at /binaries/sample.exe" echo " # Analyze a binary mounted at /binaries/sample.exe"
echo " docker run -p 8192:8192 -v ./samples:/binaries ghydramcp /binaries/sample.exe" echo " docker run -p 8192:8192 -v ./samples:/binaries mcghidra /binaries/sample.exe"
echo "" echo ""
echo " # With custom project name" echo " # With custom project name"
echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware ghydramcp /binaries/sample.exe" echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware mcghidra /binaries/sample.exe"
echo "" echo ""
echo "Environment variables:" echo "Environment variables:"
echo " GHYDRA_PORT - HTTP API port (default: 8192)" echo " MCGHIDRA_PORT - HTTP API port (default: 8192)"
echo " GHYDRA_MAXMEM - Max JVM heap (default: 2G)" echo " MCGHIDRA_MAXMEM - Max JVM heap (default: 2G)"
echo " PROJECT_NAME - Ghidra project name (default: GhydraMCP)" echo " PROJECT_NAME - Ghidra project name (default: MCGhidra)"
echo " PROJECT_DIR - Project directory (default: /projects)" echo " PROJECT_DIR - Project directory (default: /projects)"
echo "" echo ""
echo "Starting in wait mode..." echo "Starting in wait mode..."
@ -78,7 +78,7 @@ case "${GHYDRA_MODE}" in
-import "${BINARY_PATH}" -import "${BINARY_PATH}"
-max-cpu 2 -max-cpu 2
-scriptPath "${SCRIPT_DIR}" -scriptPath "${SCRIPT_DIR}"
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}" -postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}"
) )
# Add any extra arguments passed # Add any extra arguments passed
@ -93,10 +93,10 @@ case "${GHYDRA_MODE}" in
server) server)
# Server mode: Open existing project with HTTP server # Server mode: Open existing project with HTTP server
echo "Starting GhydraMCP server on existing project..." echo "Starting MCGhidra server on existing project..."
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "Usage: docker run -e GHYDRA_MODE=server ghydramcp [program_name]" echo "Usage: docker run -e MCGHIDRA_MODE=server mcghidra [program_name]"
echo "" echo ""
echo " program_name: Name of program in the project to open" echo " program_name: Name of program in the project to open"
exit 1 exit 1
@ -110,14 +110,14 @@ case "${GHYDRA_MODE}" in
-process "${PROGRAM_NAME}" \ -process "${PROGRAM_NAME}" \
-noanalysis \ -noanalysis \
-scriptPath "${SCRIPT_DIR}" \ -scriptPath "${SCRIPT_DIR}" \
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}" \ -postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}" \
"$@" "$@"
;; ;;
analyze) analyze)
# Analyze mode: Import and analyze, then exit (no HTTP server) # Analyze mode: Import and analyze, then exit (no HTTP server)
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "Usage: docker run -e GHYDRA_MODE=analyze ghydramcp [binary_path]" echo "Usage: docker run -e MCGHIDRA_MODE=analyze mcghidra [binary_path]"
exit 1 exit 1
fi fi
@ -138,7 +138,7 @@ case "${GHYDRA_MODE}" in
;; ;;
*) *)
echo "Unknown mode: ${GHYDRA_MODE}" echo "Unknown mode: ${MCGHIDRA_MODE}"
echo "Valid modes: headless, server, analyze, shell" echo "Valid modes: headless, server, analyze, shell"
exit 1 exit 1
;; ;;

20
pom.xml
View File

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

View File

@ -1,6 +1,6 @@
[project] [project]
name = "ghydramcp" name = "mcghidra"
version = "2025.12.3" version = "2026.2.11"
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" 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" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@ -15,14 +15,14 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
ghydramcp = "ghydramcp:main" mcghidra = "mcghidra:main"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/ghydramcp"] packages = ["mcghidra"]
[tool.hatch.build] [tool.hatch.build]
sources = ["src"] sources = ["src"]

View File

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

View File

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

View File

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

View File

@ -39,14 +39,14 @@ import ghidra.util.Msg;
status = PluginStatus.RELEASED, status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME, packageName = ghidra.app.DeveloperPluginPackage.NAME,
category = PluginCategoryNames.ANALYSIS, category = PluginCategoryNames.ANALYSIS,
shortDescription = "GhydraMCP Plugin for AI Analysis", shortDescription = "MCGhidra Plugin for AI Analysis",
description = "Exposes program data via HATEOAS HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).", description = "Exposes program data via HATEOAS HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).",
servicesRequired = { ProgramManager.class } servicesRequired = { ProgramManager.class }
) )
public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
// Made public static to be accessible by InstanceEndpoints // Made public static to be accessible by InstanceEndpoints
public static final Map<Integer, GhydraMCPPlugin> activeInstances = new ConcurrentHashMap<>(); public static final Map<Integer, MCGhidraPlugin> activeInstances = new ConcurrentHashMap<>();
private static final Object baseInstanceLock = new Object(); private static final Object baseInstanceLock = new Object();
private HttpServer server; private HttpServer server;
@ -54,10 +54,10 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
private boolean isBaseInstance = false; private boolean isBaseInstance = false;
/** /**
* Constructor for GhydraMCP Plugin. * Constructor for MCGhidra Plugin.
* @param tool The Ghidra PluginTool * @param tool The Ghidra PluginTool
*/ */
public GhydraMCPPlugin(PluginTool tool) { public MCGhidraPlugin(PluginTool tool) {
super(tool); super(tool);
this.port = findAvailablePort(); this.port = findAvailablePort();
@ -70,8 +70,8 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
} }
} }
Msg.info(this, "GhydraMCPPlugin loaded on port " + port); Msg.info(this, "MCGhidraPlugin loaded on port " + port);
System.out.println("[GhydraMCP] Plugin loaded on port " + port); System.out.println("[MCGhidra] Plugin loaded on port " + port);
try { try {
startServer(); startServer();
@ -111,9 +111,9 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
new Thread(() -> { new Thread(() -> {
server.start(); server.start();
Msg.info(this, "GhydraMCP HTTP server started on port " + port); Msg.info(this, "MCGhidra HTTP server started on port " + port);
System.out.println("[GhydraMCP] HTTP server started on port " + port); System.out.println("[MCGhidra] HTTP server started on port " + port);
}, "GhydraMCP-HTTP-Server").start(); }, "MCGhidra-HTTP-Server").start();
} }
/** /**
@ -350,7 +350,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
} }
Map<String, Object> rootData = new HashMap<>(); Map<String, Object> rootData = new HashMap<>();
rootData.put("message", "GhydraMCP API " + ApiConstants.API_VERSION); rootData.put("message", "MCGhidra API " + ApiConstants.API_VERSION);
rootData.put("documentation", "See GHIDRA_HTTP_API.md for full API documentation"); rootData.put("documentation", "See GHIDRA_HTTP_API.md for full API documentation");
rootData.put("isBaseInstance", isBaseInstance); rootData.put("isBaseInstance", isBaseInstance);
@ -449,8 +449,8 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
public void dispose() { public void dispose() {
if (server != null) { if (server != null) {
server.stop(0); // Stop immediately server.stop(0); // Stop immediately
Msg.info(this, "GhydraMCP HTTP server stopped on port " + port); Msg.info(this, "MCGhidra HTTP server stopped on port " + port);
System.out.println("[GhydraMCP] HTTP server stopped on port " + port); System.out.println("[MCGhidra] HTTP server stopped on port " + port);
} }
activeInstances.remove(port); activeInstances.remove(port);
super.dispose(); super.dispose();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""GhydraMCP - AI-assisted reverse engineering bridge for Ghidra. """MCGhidra - AI-assisted reverse engineering bridge for Ghidra.
A multi-instance Ghidra plugin exposed via HATEOAS REST API plus an MCP A multi-instance Ghidra plugin exposed via HATEOAS REST API plus an MCP
Python bridge for decompilation, analysis & binary manipulation. Python bridge for decompilation, analysis & binary manipulation.
@ -6,7 +6,7 @@ Python bridge for decompilation, analysis & binary manipulation.
try: try:
from importlib.metadata import version from importlib.metadata import version
__version__ = version("ghydramcp") __version__ = version("mcghidra")
except Exception: except Exception:
__version__ = "2025.12.1" __version__ = "2025.12.1"

9
src/mcghidra/__main__.py Normal file
View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""Field projection and response size guard for GhydraMCP. """Field projection and response size guard for MCGhidra.
Provides jq-style field projection, grep filtering, and token budget Provides jq-style field projection, grep filtering, and token budget
enforcement to prevent oversized MCP tool results. enforcement to prevent oversized MCP tool results.
@ -7,7 +7,7 @@ enforcement to prevent oversized MCP tool results.
import json import json
import re import re
import time import time
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional
from ..config import get_config from ..config import get_config
@ -15,7 +15,7 @@ from ..config import get_config
TOKEN_ESTIMATION_RATIO = 4.0 TOKEN_ESTIMATION_RATIO = 4.0
def project_fields(items: list, fields: list[str]) -> list: def project_fields(items: List[Any], fields: List[str]) -> List[Any]:
"""Select only specified keys from each item (jq-style projection). """Select only specified keys from each item (jq-style projection).
Works on dicts and strings. For dicts, returns only the requested Works on dicts and strings. For dicts, returns only the requested
@ -42,7 +42,7 @@ def project_fields(items: list, fields: list[str]) -> list:
return projected return projected
def apply_grep(items: list, pattern: str, ignorecase: bool = True) -> list: def apply_grep(items: List[Any], pattern: str, ignorecase: bool = True) -> List[Any]:
"""Filter items by regex pattern across all string values. """Filter items by regex pattern across all string values.
Searches all string-coercible values in each item. For dicts, Searches all string-coercible values in each item. For dicts,
@ -90,13 +90,33 @@ def _matches(item: Any, pattern: re.Pattern, depth: int = 0) -> bool:
def _estimate_tokens(data: Any) -> int: def _estimate_tokens(data: Any) -> int:
"""Estimate token count from serialized JSON size.""" """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
"""
text = json.dumps(data, default=str) text = json.dumps(data, default=str)
return int(len(text) / TOKEN_ESTIMATION_RATIO) return int(len(text) / TOKEN_ESTIMATION_RATIO)
def _extract_available_fields(items: list) -> list[str]: def _extract_available_fields(items: List[Any]) -> List[str]:
"""Extract the set of field names from the first few dict items.""" """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)
"""
fields = set() fields = set()
for item in items[:5]: for item in items[:5]:
if isinstance(item, dict): if isinstance(item, dict):
@ -107,11 +127,11 @@ def _extract_available_fields(items: list) -> list[str]:
def estimate_and_guard( def estimate_and_guard(
data: list, data: List[Any],
tool_name: str, tool_name: str,
budget: Optional[int] = None, budget: Optional[int] = None,
query_hints: Optional[Dict[str, Any]] = None, query_hints: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]: ) -> Optional[Dict[str, Any]]:
"""Check if data exceeds token budget; return guard response if so. """Check if data exceeds token budget; return guard response if so.
If data fits within budget, returns None (caller should proceed If data fits within budget, returns None (caller should proceed
@ -164,7 +184,17 @@ def estimate_and_guard(
def _format_tokens(n: int) -> str: def _format_tokens(n: int) -> str:
"""Format token count for display (e.g. 45000 -> '45k').""" """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')
"""
if n >= 1000: if n >= 1000:
return "%dk" % (n // 1000) return "%dk" % (n // 1000)
return str(n) return str(n)
@ -172,7 +202,7 @@ def _format_tokens(n: int) -> str:
def _build_hints( def _build_hints(
tool_name: str, tool_name: str,
available_fields: list[str], available_fields: List[str],
query_hints: Optional[Dict[str, Any]] = None, query_hints: Optional[Dict[str, Any]] = None,
) -> str: ) -> str:
"""Build actionable hint text for the guard message.""" """Build actionable hint text for the guard message."""

View File

@ -244,6 +244,7 @@ def safe_post(
text_payload = None text_payload = None
if isinstance(data, dict): if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None) headers = data.pop("headers", None)
json_payload = data json_payload = data
else: else:
@ -265,7 +266,11 @@ def safe_put(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
Returns: Returns:
Response dict Response dict
""" """
headers = data.pop("headers", None) if isinstance(data, dict) else None if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None)
else:
headers = None
return _make_request("PUT", port, endpoint, json_data=data, headers=headers) return _make_request("PUT", port, endpoint, json_data=data, headers=headers)
@ -280,7 +285,11 @@ def safe_patch(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]
Returns: Returns:
Response dict Response dict
""" """
headers = data.pop("headers", None) if isinstance(data, dict) else None if isinstance(data, dict):
data = data.copy() # Don't mutate caller's dict
headers = data.pop("headers", None)
else:
headers = None
return _make_request("PATCH", port, endpoint, json_data=data, headers=headers) return _make_request("PATCH", port, endpoint, json_data=data, headers=headers)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""Data mixin for GhydraMCP. """Data mixin for MCGhidra.
Provides tools for data items and strings operations. 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 fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config from ..config import get_config
from .base import GhydraMixinBase from .base import MCGhidraMixinBase
class DataMixin(GhydraMixinBase): class DataMixin(MCGhidraMixinBase):
"""Mixin for data operations. """Mixin for data operations.
Provides tools for: Provides tools for:

View File

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

View File

@ -1,4 +1,4 @@
"""Docker management mixin for GhydraMCP. """Docker management mixin for MCGhidra.
Provides tools for managing Ghidra Docker containers programmatically. Provides tools for managing Ghidra Docker containers programmatically.
Allows the MCP server to automatically start containers when Ghidra isn't available. Allows the MCP server to automatically start containers when Ghidra isn't available.
@ -19,16 +19,19 @@ from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from fastmcp import Context from fastmcp import Context
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool from fastmcp.contrib.mcp_mixin import mcp_tool
from mcghidra.core.logging import logger
from mcghidra.mixins.base import MCGhidraMixinBase
# Port pool configuration (32 ports should handle many concurrent sessions) # Port pool configuration (32 ports should handle many concurrent sessions)
PORT_POOL_START = 8192 PORT_POOL_START = 8192
PORT_POOL_END = 8223 PORT_POOL_END = 8223
PORT_LOCK_DIR = Path("/tmp/ghydramcp-ports") PORT_LOCK_DIR = Path("/tmp/mcghidra-ports")
class PortPool: class PortPool:
"""Manages a pool of ports for GhydraMCP containers. """Manages a pool of ports for MCGhidra containers.
Uses file-based locking to coordinate port allocation across multiple Uses file-based locking to coordinate port allocation across multiple
processes. Each allocated port gets a lock file that persists until processes. Each allocated port gets a lock file that persists until
@ -63,6 +66,7 @@ class PortPool:
True if port was acquired, False if already in use True if port was acquired, False if already in use
""" """
lock_path = self._lock_file(port) lock_path = self._lock_file(port)
fd = None
try: try:
# Open or create the lock file # Open or create the lock file
@ -77,6 +81,8 @@ class PortPool:
return False return False
# Write session info to the lock file # 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.ftruncate(fd, 0)
os.lseek(fd, 0, os.SEEK_SET) os.lseek(fd, 0, os.SEEK_SET)
lock_data = json.dumps({ lock_data = json.dumps({
@ -85,6 +91,10 @@ class PortPool:
"timestamp": time.time(), "timestamp": time.time(),
}) })
os.write(fd, lock_data.encode()) os.write(fd, lock_data.encode())
except Exception:
# Write failed - release the lock
os.close(fd)
raise
# Keep the file descriptor open to maintain the lock # Keep the file descriptor open to maintain the lock
# Store it so we can release later # Store it so we can release later
@ -94,7 +104,8 @@ class PortPool:
return True return True
except Exception: except Exception as e:
logger.debug("Failed to acquire port %d: %s", port, e)
return False return False
def allocate(self, session_id: str) -> Optional[int]: def allocate(self, session_id: str) -> Optional[int]:
@ -134,7 +145,8 @@ class PortPool:
lock_path.unlink() lock_path.unlink()
return True return True
except Exception: except Exception as e:
logger.debug("Failed to release port %d: %s", port, e)
return False return False
def get_allocated_ports(self) -> Dict[int, Dict[str, Any]]: def get_allocated_ports(self) -> Dict[int, Dict[str, Any]]:
@ -191,17 +203,17 @@ class PortPool:
except (IOError, OSError): except (IOError, OSError):
# Still locked by another process # Still locked by another process
os.close(fd) os.close(fd)
except Exception: except Exception as e:
pass logger.debug("Failed to check stale lock for port %d: %s", port, e)
return cleaned return cleaned
class DockerMixin(MCPMixin): class DockerMixin(MCGhidraMixinBase):
"""Docker container management for GhydraMCP. """Docker container management for MCGhidra.
Provides tools to start, stop, and manage Ghidra containers Provides tools to start, stop, and manage Ghidra containers
with the GhydraMCP plugin pre-installed. with the MCGhidra plugin pre-installed.
Supports multi-process environments with: Supports multi-process environments with:
- Dynamic port allocation from a pool (8192-8223) - Dynamic port allocation from a pool (8192-8223)
@ -219,14 +231,14 @@ class DockerMixin(MCPMixin):
# Track containers started by this session # Track containers started by this session
_session_containers: Dict[str, Dict[str, Any]] = {} _session_containers: Dict[str, Dict[str, Any]] = {}
# Label prefix for GhydraMCP containers # Label prefix for MCGhidra containers
LABEL_PREFIX = "com.ghydramcp" LABEL_PREFIX = "com.mcghidra"
def __init__(self): def __init__(self):
"""Initialize Docker mixin with session isolation.""" """Initialize Docker mixin with session isolation."""
self._check_docker_available() self._check_docker_available()
self._session_id = str(uuid.uuid4())[:8] self._session_id = str(uuid.uuid4())[:8]
self._port_pool = PortPool() self._port_pool = None # Lazy-init to avoid side effects
self._session_containers = {} self._session_containers = {}
@property @property
@ -236,14 +248,25 @@ class DockerMixin(MCPMixin):
self._session_id = str(uuid.uuid4())[:8] self._session_id = str(uuid.uuid4())[:8]
return self._session_id 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: def _check_docker_available(self) -> bool:
"""Check if Docker is available on the system.""" """Check if Docker is available on the system."""
return shutil.which("docker") is not None return shutil.which("docker") is not None
def _run_docker_cmd( def _run_docker_cmd_sync(
self, args: List[str], check: bool = True, capture: bool = True self, args: List[str], check: bool = True, capture: bool = True
) -> subprocess.CompletedProcess: ) -> subprocess.CompletedProcess:
"""Run a docker command. """Run a docker command synchronously (internal use only).
Args: Args:
args: Command arguments (after 'docker') args: Command arguments (after 'docker')
@ -261,6 +284,25 @@ class DockerMixin(MCPMixin):
text=True, 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( def _run_compose_cmd(
self, self,
args: List[str], args: List[str],
@ -289,7 +331,7 @@ class DockerMixin(MCPMixin):
env = os.environ.copy() env = os.environ.copy()
if project_dir: if project_dir:
env["COMPOSE_PROJECT_NAME"] = "ghydramcp" env["COMPOSE_PROJECT_NAME"] = "mcghidra"
return subprocess.run( return subprocess.run(
cmd, cmd,
@ -303,7 +345,7 @@ class DockerMixin(MCPMixin):
def _generate_container_name(self, binary_name: str) -> str: def _generate_container_name(self, binary_name: str) -> str:
"""Generate a unique container name for this session. """Generate a unique container name for this session.
Format: ghydramcp-{session_id}-{binary_stem} Format: mcghidra-{session_id}-{binary_stem}
Args: Args:
binary_name: Name of the binary being analyzed binary_name: Name of the binary being analyzed
@ -314,7 +356,7 @@ class DockerMixin(MCPMixin):
# Clean binary name for container naming # Clean binary name for container naming
stem = Path(binary_name).stem.lower() stem = Path(binary_name).stem.lower()
clean_name = "".join(c if c.isalnum() else "-" for c in stem)[:20] clean_name = "".join(c if c.isalnum() else "-" for c in stem)[:20]
return f"ghydramcp-{self.session_id}-{clean_name}" return f"mcghidra-{self.session_id}-{clean_name}"
def _get_container_labels(self, binary_path: str, port: int) -> Dict[str, str]: def _get_container_labels(self, binary_path: str, port: int) -> Dict[str, str]:
"""Generate Docker labels for a container. """Generate Docker labels for a container.
@ -336,12 +378,12 @@ class DockerMixin(MCPMixin):
f"{self.LABEL_PREFIX}.pid": str(os.getpid()), f"{self.LABEL_PREFIX}.pid": str(os.getpid()),
} }
def _find_containers_by_label( async def _find_containers_by_label(
self, self,
label_filter: Optional[str] = None, label_filter: Optional[str] = None,
session_only: bool = False, session_only: bool = False,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Find GhydraMCP containers by label. """Find MCGhidra containers by label.
Args: Args:
label_filter: Additional label filter (e.g., "port=8192") label_filter: Additional label filter (e.g., "port=8192")
@ -359,7 +401,7 @@ class DockerMixin(MCPMixin):
if label_filter: if label_filter:
filter_args.extend(["--filter", f"label={self.LABEL_PREFIX}.{label_filter}"]) filter_args.extend(["--filter", f"label={self.LABEL_PREFIX}.{label_filter}"])
ps_result = self._run_docker_cmd( ps_result = await self._run_docker_cmd(
[ [
"ps", "-a", "ps", "-a",
*filter_args, *filter_args,
@ -389,19 +431,19 @@ class DockerMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="docker_status", name="docker_status",
description="Check Docker availability and running GhydraMCP containers", description="Check Docker availability and running MCGhidra containers",
) )
async def docker_status(self, ctx: Optional[Context] = None) -> Dict[str, Any]: async def docker_status(self, ctx: Optional[Context] = None) -> Dict[str, Any]:
"""Check Docker status and list running GhydraMCP containers. """Check Docker status and list running MCGhidra containers.
Returns: Returns:
Status information including: Status information including:
- docker_available: Whether Docker is installed - docker_available: Whether Docker is installed
- docker_running: Whether Docker daemon is running - docker_running: Whether Docker daemon is running
- session_id: This MCP instance's session ID - session_id: This MCP instance's session ID
- containers: List of GhydraMCP containers with their status - containers: List of MCGhidra containers with their status
- port_pool: Port allocation status - port_pool: Port allocation status
- images: Available GhydraMCP images - images: Available MCGhidra images
""" """
result = { result = {
"docker_available": False, "docker_available": False,
@ -425,36 +467,35 @@ class DockerMixin(MCPMixin):
# Check if docker daemon is running # Check if docker daemon is running
try: try:
self._run_docker_cmd(["info"], check=True) await self._run_docker_cmd(["info"], check=True)
result["docker_running"] = True result["docker_running"] = True
except (subprocess.CalledProcessError, FileNotFoundError): except (subprocess.CalledProcessError, FileNotFoundError):
return result return result
# Check for docker compose # Check for docker compose
try: try:
self._run_docker_cmd(["compose", "version"], check=True) await self._run_docker_cmd(["compose", "version"], check=True)
result["compose_available"] = True result["compose_available"] = True
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass
# List all GhydraMCP containers (from any session) # List all MCGhidra containers (from any session)
result["containers"] = self._find_containers_by_label() result["containers"] = await self._find_containers_by_label()
# List containers from this session only # List containers from this session only
result["session_containers"] = self._find_containers_by_label(session_only=True) result["session_containers"] = await self._find_containers_by_label(session_only=True)
# Get port pool status # Get port pool status (lazy-init creates pool on first access)
if self._port_pool: result["port_pool"]["allocated"] = self.port_pool.get_allocated_ports()
result["port_pool"]["allocated"] = self._port_pool.get_allocated_ports()
# Also check by name pattern for containers without labels # Also check by name pattern for containers without labels
try: try:
ps_result = self._run_docker_cmd( ps_result = await self._run_docker_cmd(
[ [
"ps", "ps",
"-a", "-a",
"--filter", "--filter",
"name=ghydramcp", "name=mcghidra",
"--format", "--format",
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}", "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}",
] ]
@ -476,13 +517,13 @@ class DockerMixin(MCPMixin):
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass
# List GhydraMCP images # List MCGhidra images
try: try:
images_result = self._run_docker_cmd( images_result = await self._run_docker_cmd(
[ [
"images", "images",
"--filter", "--filter",
"reference=ghydramcp*", "reference=mcghidra*",
"--format", "--format",
"{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}", "{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}",
] ]
@ -505,7 +546,7 @@ class DockerMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="docker_start", name="docker_start",
description="Start a GhydraMCP Docker container to analyze a binary (auto-assigns port from pool)", description="Start a MCGhidra Docker container to analyze a binary (auto-assigns port from pool)",
) )
async def docker_start( async def docker_start(
self, self,
@ -514,9 +555,9 @@ class DockerMixin(MCPMixin):
name: Optional[str] = None, name: Optional[str] = None,
ctx: Optional[Context] = None, ctx: Optional[Context] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Start a GhydraMCP Docker container for binary analysis. """Start a MCGhidra Docker container for binary analysis.
This creates a new Ghidra instance in Docker with the GhydraMCP This creates a new Ghidra instance in Docker with the MCGhidra
plugin pre-installed. The binary will be imported and analyzed, plugin pre-installed. The binary will be imported and analyzed,
then the HTTP API will be available. then the HTTP API will be available.
@ -540,14 +581,6 @@ class DockerMixin(MCPMixin):
if not binary_file.exists(): if not binary_file.exists():
return {"error": f"Binary not found: {binary_path}"} 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 # Generate container name if not specified
if name is None: if name is None:
name = self._generate_container_name(binary_file.name) name = self._generate_container_name(binary_file.name)
@ -557,23 +590,42 @@ class DockerMixin(MCPMixin):
try: try:
# Check if container with this name already exists # Check if container with this name already exists
check_result = self._run_docker_cmd( check_result = await self._run_docker_cmd(
["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False ["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False
) )
if check_result.stdout.strip(): if check_result.stdout.strip():
self._port_pool.release(port)
return { return {
"error": f"Container '{name}' already exists. Stop it first with docker_stop." "error": f"Container '{name}' already exists. Stop it first with docker_stop."
} }
# Check if port is already in use by a non-pool container # Allocate a port that's both lockable AND not in use by Docker
port_check = self._run_docker_cmd( # This handles external containers (not managed by MCGhidra) using ports in our range
["ps", "-q", "-f", f"publish={port}"], check=False 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(): if port_check.stdout.strip():
self._port_pool.release(port) # 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:
return { return {
"error": f"Port {port} is already in use by another container" "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(),
} }
# Build label arguments # Build label arguments
@ -583,7 +635,7 @@ class DockerMixin(MCPMixin):
label_args.extend(["-l", f"{k}={v}"]) label_args.extend(["-l", f"{k}={v}"])
# Start the container # Start the container
run_result = self._run_docker_cmd( run_result = await self._run_docker_cmd(
[ [
"run", "run",
"-d", "-d",
@ -594,9 +646,9 @@ class DockerMixin(MCPMixin):
"-v", "-v",
f"{binary_file.parent}:/binaries:ro", f"{binary_file.parent}:/binaries:ro",
"-e", "-e",
f"GHYDRA_MAXMEM={memory}", f"MCGHIDRA_MAXMEM={memory}",
*label_args, *label_args,
"ghydramcp:latest", "mcghidra:latest",
f"/binaries/{binary_file.name}", f"/binaries/{binary_file.name}",
] ]
) )
@ -627,17 +679,17 @@ class DockerMixin(MCPMixin):
} }
except subprocess.CalledProcessError as e: 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}"} return {"error": f"Failed to start container: {e.stderr or e.stdout}"}
@mcp_tool( @mcp_tool(
name="docker_stop", name="docker_stop",
description="Stop a running GhydraMCP Docker container", description="Stop a running MCGhidra Docker container",
) )
async def docker_stop( async def docker_stop(
self, name_or_id: str, remove: bool = True, ctx: Optional[Context] = None self, name_or_id: str, remove: bool = True, ctx: Optional[Context] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Stop a GhydraMCP Docker container. """Stop a MCGhidra Docker container.
For safety, this will only stop containers that belong to the current For safety, this will only stop containers that belong to the current
MCP session. Attempting to stop another session's container will fail MCP session. Attempting to stop another session's container will fail
@ -657,7 +709,7 @@ class DockerMixin(MCPMixin):
container_port = None container_port = None
container_session = None container_session = None
try: try:
inspect_result = self._run_docker_cmd( inspect_result = await self._run_docker_cmd(
[ [
"inspect", "inspect",
"--format", "--format",
@ -683,14 +735,14 @@ class DockerMixin(MCPMixin):
try: try:
# Stop the container # Stop the container
self._run_docker_cmd(["stop", name_or_id]) await self._run_docker_cmd(["stop", name_or_id])
if remove: if remove:
self._run_docker_cmd(["rm", name_or_id]) await self._run_docker_cmd(["rm", name_or_id])
# Release the port back to the pool # Release the port back to the pool
if container_port: if container_port:
self._port_pool.release(container_port) self.port_pool.release(container_port)
# Remove from session tracking # Remove from session tracking
self._session_containers = { self._session_containers = {
@ -711,7 +763,7 @@ class DockerMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="docker_logs", name="docker_logs",
description="Get logs from a GhydraMCP Docker container", description="Get logs from a MCGhidra Docker container",
) )
async def docker_logs( async def docker_logs(
self, self,
@ -720,7 +772,7 @@ class DockerMixin(MCPMixin):
follow: bool = False, follow: bool = False,
ctx: Optional[Context] = None, ctx: Optional[Context] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get logs from a GhydraMCP container. """Get logs from a MCGhidra container.
Args: Args:
name_or_id: Container name or ID name_or_id: Container name or ID
@ -739,7 +791,7 @@ class DockerMixin(MCPMixin):
args.append("-f") args.append("-f")
args.append(name_or_id) args.append(name_or_id)
result = self._run_docker_cmd(args) result = await self._run_docker_cmd(args)
return { return {
"success": True, "success": True,
"container": name_or_id, "container": name_or_id,
@ -751,7 +803,7 @@ class DockerMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="docker_build", name="docker_build",
description="Build the GhydraMCP Docker image from source", description="Build the MCGhidra Docker image from source",
) )
async def docker_build( async def docker_build(
self, self,
@ -760,12 +812,12 @@ class DockerMixin(MCPMixin):
project_dir: Optional[str] = None, project_dir: Optional[str] = None,
ctx: Optional[Context] = None, ctx: Optional[Context] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Build the GhydraMCP Docker image. """Build the MCGhidra Docker image.
Args: Args:
tag: Image tag (default: 'latest') tag: Image tag (default: 'latest')
no_cache: Build without using cache no_cache: Build without using cache
project_dir: Path to GhydraMCP project (auto-detected if not specified) project_dir: Path to MCGhidra project (auto-detected if not specified)
Returns: Returns:
Build status Build status
@ -783,7 +835,7 @@ class DockerMixin(MCPMixin):
proj_path = module_dir proj_path = module_dir
else: else:
return { return {
"error": "Could not find GhydraMCP project directory. Please specify project_dir." "error": "Could not find MCGhidra project directory. Please specify project_dir."
} }
dockerfile = proj_path / "docker" / "Dockerfile" dockerfile = proj_path / "docker" / "Dockerfile"
@ -794,7 +846,7 @@ class DockerMixin(MCPMixin):
args = [ args = [
"build", "build",
"-t", "-t",
f"ghydramcp:{tag}", f"mcghidra:{tag}",
"-f", "-f",
str(dockerfile), str(dockerfile),
] ]
@ -803,12 +855,12 @@ class DockerMixin(MCPMixin):
args.append(str(proj_path)) args.append(str(proj_path))
# Run build (this can take a while) # Run build (this can take a while)
result = self._run_docker_cmd(args, capture=True) result = await self._run_docker_cmd(args, capture=True)
return { return {
"success": True, "success": True,
"image": f"ghydramcp:{tag}", "image": f"mcghidra:{tag}",
"message": f"Successfully built ghydramcp:{tag}", "message": f"Successfully built mcghidra:{tag}",
"output": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout, "output": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout,
} }
@ -858,75 +910,32 @@ class DockerMixin(MCPMixin):
@mcp_tool( @mcp_tool(
name="docker_health", name="docker_health",
description="Check if a GhydraMCP container's API is responding", description="Check if a MCGhidra container's API is responding",
) )
async def docker_health( async def docker_health(
self, port: int = 8192, timeout: float = 5.0, ctx: Optional[Context] = None self, port: Optional[int] = None, timeout: float = 5.0, ctx: Optional[Context] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Check if a GhydraMCP container's API is healthy. """Check if a MCGhidra container's API is healthy.
Args: Args:
port: API port to check (default: 8192) port: API port to check (uses current instance if not specified)
timeout: Request timeout in seconds timeout: Request timeout in seconds
Returns: Returns:
Health status and API info if available Health status and API info if available
""" """
loop = asyncio.get_event_loop() port = self.get_instance_port(port)
return await loop.run_in_executor( return await asyncio.get_running_loop().run_in_executor(
None, self._sync_health_check, port, timeout 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( @mcp_tool(
name="docker_auto_start", name="docker_auto_start",
description="Automatically start a GhydraMCP container with dynamic port allocation", description="Automatically start a MCGhidra container with dynamic port allocation",
) )
async def docker_auto_start( async def docker_auto_start(
self, self,
binary_path: str, binary_path: str,
wait: bool = False,
timeout: float = 300.0,
ctx: Optional[Context] = None, ctx: Optional[Context] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Automatically start a Docker container with intelligent port allocation. """Automatically start a Docker container with intelligent port allocation.
@ -934,19 +943,20 @@ class DockerMixin(MCPMixin):
This is the main entry point for automatic Docker management: This is the main entry point for automatic Docker management:
1. Checks if a Ghidra instance with the SAME binary is already running 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 2. If not, allocates a port from the pool and starts a new container
3. Optionally waits for the container to become healthy 3. Returns connection info immediately
4. Returns connection info for the instance
Ports are auto-allocated from the pool (8192-8223) to prevent Ports are auto-allocated from the pool (8192-8223) to prevent
conflicts between concurrent sessions. 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: Args:
binary_path: Path to the binary to analyze 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: Returns:
Instance connection info with session ID and port details Instance connection info with session ID and port details.
Poll docker_health(port) to check when container is ready.
""" """
import os import os
@ -979,10 +989,10 @@ class DockerMixin(MCPMixin):
} }
# Check if we have the image # Check if we have the image
if not any("ghydramcp" in img.get("name", "") for img in status.get("images", [])): if not any("mcghidra" in img.get("name", "") for img in status.get("images", [])):
return { return {
"error": ( "error": (
"GhydraMCP Docker image not found. " "MCGhidra Docker image not found. "
"Build it with docker_build() or 'make build' first." "Build it with docker_build() or 'make build' first."
) )
} }
@ -997,10 +1007,6 @@ class DockerMixin(MCPMixin):
actual_port = start_result.get("port") 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 { return {
"source": "docker", "source": "docker",
"session_id": self.session_id, "session_id": self.session_id,
@ -1008,28 +1014,7 @@ class DockerMixin(MCPMixin):
"container_name": start_result.get("name"), "container_name": start_result.get("name"),
"port": actual_port, "port": actual_port,
"api_url": f"http://localhost:{actual_port}/", "api_url": f"http://localhost:{actual_port}/",
"program": wait_result.get("program"), "message": f"Container starting on port {actual_port}. Poll docker_health(port={actual_port}), then call instances_use(port={actual_port}) when healthy.",
"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,
"container_id": start_result.get("container_id"),
"container_name": start_result.get("name"),
"port": actual_port,
"api_url": f"http://localhost:{actual_port}/",
"message": f"Container starting on port {actual_port}. Use docker_wait() or docker_health() to check status.",
} }
@mcp_tool( @mcp_tool(
@ -1043,14 +1028,14 @@ class DockerMixin(MCPMixin):
dry_run: bool = False, dry_run: bool = False,
ctx: Optional[Context] = None, ctx: Optional[Context] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Clean up orphaned GhydraMCP containers and stale port locks. """Clean up orphaned MCGhidra containers and stale port locks.
This helps recover from crashed processes that left containers or This helps recover from crashed processes that left containers or
port locks behind. port locks behind.
By default, only cleans containers from the current session to prevent By default, only cleans containers from the current session to prevent
accidentally removing another agent's work. Set session_only=False accidentally removing another agent's work. Set session_only=False
(with caution) to clean all GhydraMCP containers. (with caution) to clean all MCGhidra containers.
Args: Args:
session_only: Only clean up containers from this session (default: True for safety) session_only: Only clean up containers from this session (default: True for safety)
@ -1071,12 +1056,12 @@ class DockerMixin(MCPMixin):
} }
# Find orphaned containers # Find orphaned containers
containers = self._find_containers_by_label(session_only=session_only) containers = await self._find_containers_by_label(session_only=session_only)
for container in containers: for container in containers:
# Check if container is old enough to be considered orphaned # Check if container is old enough to be considered orphaned
try: try:
inspect_result = self._run_docker_cmd( inspect_result = await self._run_docker_cmd(
["inspect", "--format", "{{index .Config.Labels \"" + self.LABEL_PREFIX + ".started\"}}", container["id"]], ["inspect", "--format", "{{index .Config.Labels \"" + self.LABEL_PREFIX + ".started\"}}", container["id"]],
check=False, check=False,
) )
@ -1106,8 +1091,7 @@ class DockerMixin(MCPMixin):
pass pass
# Clean up stale port locks # Clean up stale port locks
if self._port_pool: stale_ports = self.port_pool.cleanup_stale_locks(max_age_hours * 3600)
stale_ports = self._port_pool.cleanup_stale_locks(max_age_hours * 3600)
result["ports_cleaned"] = stale_ports result["ports_cleaned"] = stale_ports
return result return result
@ -1132,8 +1116,8 @@ class DockerMixin(MCPMixin):
"containers": self._session_containers, "containers": self._session_containers,
"allocated_ports": { "allocated_ports": {
port: info 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 info.get("session_id") == self.session_id
} if self._port_pool else {}, },
"port_pool_range": f"{PORT_POOL_START}-{PORT_POOL_END}", "port_pool_range": f"{PORT_POOL_START}-{PORT_POOL_END}",
} }

View File

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

View File

@ -1,4 +1,4 @@
"""Instance management mixin for GhydraMCP. """Instance management mixin for MCGhidra.
Provides tools for discovering, registering, and managing Ghidra instances. 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 fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config from ..config import get_config
from .base import GhydraMixinBase from .base import MCGhidraMixinBase
class InstancesMixin(GhydraMixinBase): class InstancesMixin(MCGhidraMixinBase):
"""Mixin for Ghidra instance management. """Mixin for Ghidra instance management.
Provides tools for: Provides tools for:

View File

@ -1,4 +1,4 @@
"""Memory mixin for GhydraMCP. """Memory mixin for MCGhidra.
Provides tools for memory read/write operations. 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 fastmcp.contrib.mcp_mixin import mcp_tool
from .base import GhydraMixinBase from .base import MCGhidraMixinBase
class MemoryMixin(GhydraMixinBase): class MemoryMixin(MCGhidraMixinBase):
"""Mixin for memory operations. """Mixin for memory operations.
Provides tools for: Provides tools for:

View File

@ -1,4 +1,4 @@
"""Namespaces mixin for GhydraMCP. """Namespaces mixin for MCGhidra.
Provides tools for querying namespaces and class definitions. 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 fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config from ..config import get_config
from .base import GhydraMixinBase from .base import MCGhidraMixinBase
class NamespacesMixin(GhydraMixinBase): class NamespacesMixin(MCGhidraMixinBase):
"""Mixin for namespace and class operations. """Mixin for namespace and class operations.
Provides tools for: Provides tools for:

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""Cross-references mixin for GhydraMCP. """Cross-references mixin for MCGhidra.
Provides tools for cross-reference (xref) operations. 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 fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
from ..config import get_config from ..config import get_config
from .base import GhydraMixinBase from .base import MCGhidraMixinBase
class XrefsMixin(GhydraMixinBase): class XrefsMixin(MCGhidraMixinBase):
"""Mixin for cross-reference operations. """Mixin for cross-reference operations.
Provides tools for: Provides tools for:

View File

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

View File

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

View File

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

View File

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

View File

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

38
uv.lock generated
View File

@ -414,25 +414,6 @@ 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" }, { 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]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.3.1" version = "3.3.1"
@ -589,6 +570,25 @@ 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" }, { 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]] [[package]]
name = "mcp" name = "mcp"
version = "1.23.1" version = "1.23.1"