Compare commits
71 Commits
v2.0.0-bet
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 38df6ee12a | |||
| 1db36464ed | |||
| 0250c2df01 | |||
| 83949683ae | |||
| 14b2b575c8 | |||
| f4cf1cef9e | |||
| 112c1969c8 | |||
| 57f042a802 | |||
| 842035ca92 | |||
| c930e7c059 | |||
| 1143489924 | |||
| d1750cb339 | |||
| 04f3011413 | |||
| 41bd8445e9 | |||
| d298a89f5f | |||
| 5300fb24b8 | |||
| 6662c8411a | |||
| f1986db6cc | |||
| 7eefdda9f8 | |||
| 19f1428fb6 | |||
| 48ccc2aff3 | |||
| 77ce01d313 | |||
| d1f8779f05 | |||
| 458d4fb35b | |||
| c93abaf86c | |||
| 0d25a0dc24 | |||
| 1b42ab251e | |||
| 290252c0db | |||
| 70f226f68e | |||
| 4c112a2421 | |||
| 2d837d95fc | |||
| 28b81ff359 | |||
| a3ad70d302 | |||
| 88e1fe6ca8 | |||
| ee82f3b100 | |||
| ac06111288 | |||
| f640df70ca | |||
| c747abe813 | |||
|
|
662e202482 | ||
|
|
60124d2315 | ||
|
|
f32dc5504c | ||
|
|
afc09f1cd9 | ||
|
|
98a3ec4c34 | ||
|
|
8268e55a08 | ||
|
|
30d9bb17da | ||
|
|
f4212b8666 | ||
|
|
301c1849f8 | ||
|
|
24f5f1698a | ||
|
|
0c451a447c | ||
|
|
3222cf9866 | ||
|
|
bc1e137878 | ||
|
|
4379bea14f | ||
|
|
977791432f | ||
|
|
2ec406b913 | ||
|
|
52312f9ed3 | ||
|
|
f8cf202a50 | ||
|
|
f04223d23a | ||
|
|
89fa811284 | ||
|
|
384f8b272f | ||
|
|
b37be370f8 | ||
|
|
8313b2bd7b | ||
|
|
0f9aa2bb47 | ||
|
|
8aded2e6c3 | ||
|
|
58f22316bd | ||
|
|
5e1b1d4cbd | ||
|
|
24f84fc4ea | ||
|
|
f377a34442 | ||
|
|
c4d170cdca | ||
|
|
7cf426ef53 | ||
|
|
f71f4aa43b | ||
|
|
25f353a4f3 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -50,3 +50,10 @@ hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
/__pycache__
|
||||
|
||||
# Docker volumes and binaries
|
||||
/binaries/*
|
||||
!/binaries/.gitkeep
|
||||
|
||||
# Ghidra source (fetched separately)
|
||||
/ghidra-src/
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11
|
||||
115
BUG_REPORT_HEADLESS_GSON.md
Normal file
115
BUG_REPORT_HEADLESS_GSON.md
Normal file
@ -0,0 +1,115 @@
|
||||
# Bug Report: Docker Headless Mode Fails - Missing Gson Dependency
|
||||
|
||||
## Summary
|
||||
|
||||
The MCGhidra Docker container fails to start the HTTP API server because `MCGhidraServer.java` imports Gson, but Gson is not available in Ghidra's headless script classpath.
|
||||
|
||||
## Environment
|
||||
|
||||
- MCGhidra Docker image: `mcghidra:latest`
|
||||
- Ghidra Version: 11.4.2
|
||||
- Build Date: 2025-08-26
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1. Build the Docker image:
|
||||
```bash
|
||||
docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
2. Run with a binary:
|
||||
```bash
|
||||
docker run -p 8192:8192 -v /path/to/binary:/binaries/test mcghidra:latest /binaries/test
|
||||
```
|
||||
|
||||
3. Check logs:
|
||||
```bash
|
||||
docker logs <container_id>
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
Container should start and expose HTTP API on port 8192.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
Analysis completes but the script fails to load:
|
||||
|
||||
```
|
||||
INFO REPORT: Analysis succeeded for file: file:///binaries/cardv (HeadlessAnalyzer)
|
||||
ERROR REPORT SCRIPT ERROR: MCGhidraServer.java : The class could not be found.
|
||||
It must be the public class of the .java file: Failed to get OSGi bundle containing script:
|
||||
/opt/ghidra/scripts/MCGhidraServer.java (HeadlessAnalyzer)
|
||||
```
|
||||
|
||||
The health check fails because the HTTP server never starts:
|
||||
|
||||
```json
|
||||
{"healthy":false,"port":8192,"error":"[Errno 111] Connection refused"}
|
||||
```
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
`MCGhidraServer.java` (lines 22-24) imports Gson:
|
||||
|
||||
```java
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
```
|
||||
|
||||
However:
|
||||
1. Gson is **not** bundled with Ghidra
|
||||
2. The MCGhidra extension JAR includes Gson, but headless scripts run in a **separate OSGi classloader** without access to extension lib dependencies
|
||||
3. The Dockerfile doesn't copy Gson to Ghidra's script classpath
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Check if Gson is in the built extension
|
||||
unzip -l target/MCGhidra-*.zip | grep -i gson
|
||||
# Result: No matches
|
||||
|
||||
# Check Ghidra's lib directories
|
||||
ls /opt/ghidra/Ghidra/Framework/*/lib/ | grep -i gson
|
||||
# Result: No matches
|
||||
```
|
||||
|
||||
## Proposed Solutions
|
||||
|
||||
### Option 1: Bundle Gson JAR with Scripts (Recommended)
|
||||
|
||||
Add Gson JAR to Ghidra's script classpath in Dockerfile:
|
||||
|
||||
```dockerfile
|
||||
# Download Gson and add to Ghidra lib
|
||||
RUN curl -fsSL "https://repo1.maven.org/maven2/com/google/gson/gson/2.10.1/gson-2.10.1.jar" \
|
||||
-o /opt/ghidra/Ghidra/Framework/Generic/lib/gson-2.10.1.jar
|
||||
```
|
||||
|
||||
### Option 2: Use Built-in JSON (No External Dependencies)
|
||||
|
||||
Rewrite `MCGhidraServer.java` to use only JDK classes:
|
||||
- Replace Gson with `javax.json` or manual JSON string building
|
||||
- This ensures the script works without any external dependencies
|
||||
|
||||
### Option 3: Pre-compiled Script JAR
|
||||
|
||||
Compile `MCGhidraServer.java` with Gson into a JAR and place it in the extension, then reference it differently in headless mode.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Severity**: High - Docker deployment is completely broken
|
||||
- **Affected**: All users attempting to use Docker/headless mode
|
||||
- **Workaround**: None currently (must use GUI mode)
|
||||
|
||||
## Additional Context
|
||||
|
||||
The main MCGhidra plugin works fine in GUI mode because the extension's lib dependencies are loaded. This only affects the headless Docker workflow where scripts are loaded separately from the extension.
|
||||
|
||||
---
|
||||
|
||||
**Reported by**: Firmware analysis session
|
||||
**Date**: 2026-01-26
|
||||
**Binary being analyzed**: WOLFBOX G850 dashcam `cardv` (ARM 32-bit)
|
||||
159
CHANGELOG.md
159
CHANGELOG.md
@ -7,6 +7,147 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Symbol CRUD Operations:** Full create/rename/delete support for symbols and labels:
|
||||
- `symbols_create` - Create new label/symbol at an address
|
||||
- `symbols_rename` - Rename existing symbol
|
||||
- `symbols_delete` - Delete symbol at an address
|
||||
- `symbols_imports` - List imported symbols with pagination
|
||||
- `symbols_exports` - List exported symbols with pagination
|
||||
- **Bookmark Management:** Tools for managing Ghidra bookmarks:
|
||||
- `bookmarks_list` - List bookmarks with type/category filtering
|
||||
- `bookmarks_create` - Create bookmark at address (Note, Warning, Error, Info types)
|
||||
- `bookmarks_delete` - Delete bookmarks at an address
|
||||
- **Enum & Typedef Creation:** Data type creation tools:
|
||||
- `enums_create` - Create new enum data type
|
||||
- `enums_list` - List enum types with members
|
||||
- `typedefs_create` - Create new typedef
|
||||
- `typedefs_list` - List typedef data types
|
||||
- **Variable Management:** Enhanced variable operations:
|
||||
- `variables_list` - List variables with global_only filter
|
||||
- `variables_rename` - Rename and retype function variables
|
||||
- `functions_variables` - List local variables and parameters for a function
|
||||
- **Namespace & Class Tools:**
|
||||
- `namespaces_list` - List all non-global namespaces
|
||||
- `classes_list` - List class namespaces with qualified names
|
||||
- **Memory Segment Tools:**
|
||||
- `segments_list` - List memory segments with R/W/X permissions and size info
|
||||
- **Progress Reporting for Long Operations:** 7 MCP prompts now report real-time progress during multi-step scanning operations:
|
||||
- `malware_triage` - Reports progress across 21 scanning steps
|
||||
- `analyze_imports` - Reports progress across 12 capability categories
|
||||
- `identify_crypto` - Reports progress across 20 pattern scans
|
||||
- `find_authentication` - Reports progress across 30 auth pattern scans
|
||||
- `find_main_logic` - Reports progress across 22 entry point searches
|
||||
- `find_error_handlers` - Reports progress across 35 error pattern scans
|
||||
- `find_config_parsing` - Reports progress across 23 config pattern scans
|
||||
- Uses FastMCP's `Context.report_progress()` for numeric progress updates
|
||||
- Uses `Context.info()` for descriptive step notifications
|
||||
- Helper functions `report_step()` and `report_progress()` for consistent reporting
|
||||
- **Specialized Analysis Prompts:** 13 new MCP prompts for common reverse engineering workflows:
|
||||
- `analyze_strings` - String analysis with categorization and cross-reference guidance
|
||||
- `trace_data_flow` - Data flow and taint analysis through functions
|
||||
- `identify_crypto` - Cryptographic function and constant identification
|
||||
- `malware_triage` - Quick malware analysis with capability assessment checklist
|
||||
- `analyze_protocol` - Network/file protocol reverse engineering framework
|
||||
- `find_main_logic` - Navigate past CRT initialization to find actual program logic
|
||||
- `analyze_imports` - Categorize imports by capability with suspicious pattern detection
|
||||
- `find_authentication` - Locate auth, license checks, and credential handling code
|
||||
- `analyze_switch_table` - Reverse engineer command dispatchers and jump tables
|
||||
- `find_config_parsing` - Identify configuration file parsing and settings management
|
||||
- `compare_functions` - Compare two functions for similarity (patches, variants, libraries)
|
||||
- `document_struct` - Comprehensively document data structure fields and usage
|
||||
- `find_error_handlers` - Map error handling, cleanup routines, and exit paths
|
||||
|
||||
### Changed
|
||||
- **Docker Port Allocation:** Ports are now auto-allocated from pool (8192-8223) instead of client-specified. Prevents session collisions in multi-agent environments.
|
||||
- **docker_auto_start:** Removed `wait` and `timeout` parameters. Always returns immediately after starting container.
|
||||
- **Removed docker_wait tool:** This tool blocked for up to 5 minutes in a single call. LLMs should poll `docker_health(port)` in their own loop instead — this gives visibility into progress and ability to check logs between polls.
|
||||
|
||||
### Fixed
|
||||
- **instances_use Hanging:** Eliminated 4+ hour hangs by removing blocking HTTP call. Now uses lazy registration — just creates a stub entry, validates on first real tool call.
|
||||
- **All Docker Operations Non-Blocking:** ALL Docker subprocess calls (`docker ps`, `docker run`, `docker stop`, etc.) now run in thread executor via `run_in_executor()`. Previously only `docker_health` was fixed, but `docker_status`, `docker_start`, `docker_stop`, `docker_logs`, `docker_build`, and `docker_cleanup` still blocked the event loop. This caused `docker_auto_start(wait=True)` to freeze the MCP server.
|
||||
- **Session Isolation:** `docker_stop` now validates container belongs to current session before stopping. `docker_cleanup` defaults to `session_only=True` to prevent cross-session interference.
|
||||
- **Background Discovery Thread:** Fixed timeout from 30s to 0.5s for port scanning, reducing discovery cycle from 300s+ to ~15s.
|
||||
- **Typedef/Variable Type Resolution:** Fixed `handle_typedef_create` and `handle_variable_rename` to use shared `resolve_data_type()` for builtin types (int, char, etc.).
|
||||
- **DockerMixin Inheritance:** Fixed crash when `DockerMixin` called `get_instance_port()` — was inheriting from wrong base class.
|
||||
- **Deprecated asyncio API:** Replaced `asyncio.get_event_loop()` with `asyncio.get_running_loop()` for Python 3.10+ compatibility.
|
||||
- **HTTP Client Data Mutation:** `safe_post`, `safe_put`, and `safe_patch` no longer mutate the caller's data dict via `.pop()`.
|
||||
- **Race Condition in Discovery:** Initial instance discovery in `main()` now uses `_instances_lock` for thread safety.
|
||||
- **Silent Exception Handling:** Added debug logging to PortPool exception handlers and analysis fallback paths.
|
||||
- **File Descriptor Leak:** Fixed potential leak in `PortPool._try_acquire_port()` if write operations fail after lock acquisition.
|
||||
- **Hash Algorithm Consistency:** Changed query hash from MD5 to SHA-256 in pagination module for consistency with cursor ID generation.
|
||||
- **Lazy PortPool Initialization:** `PortPool` now created on first use, avoiding `/tmp/mcghidra-ports` directory creation when Docker tools are never used.
|
||||
- **Logging Configuration:** `configure_logging()` now called during server startup — debug messages actually work now.
|
||||
- **Type Hint Consistency:** Aligned `filtering.py` to use `List[T]` from typing module like rest of codebase.
|
||||
- **Parameter Naming:** Renamed `project_fields` to `fields` in `structs_get()` for consistency with other tools.
|
||||
- **Import Path:** Fixed `logging.py` to import `Context` from `fastmcp` (not deprecated `mcp.server.fastmcp` path).
|
||||
|
||||
### Added
|
||||
- **Debug Logging Environment Variable:** Set `MCGHIDRA_DEBUG=1` to enable DEBUG-level logging for troubleshooting.
|
||||
|
||||
## [2025.12.1] - 2025-12-01
|
||||
|
||||
### Added
|
||||
- **Cursor-Based Pagination System:** Implemented efficient pagination for large responses (10K+ items) without filling context windows.
|
||||
- `page_size` parameter (default: 50, max: 500) for controlling items per page
|
||||
- `cursor_id` returned for navigating to subsequent pages
|
||||
- Session isolation prevents cursor cross-contamination between MCP clients
|
||||
- TTL-based cursor expiration (5 minutes) with LRU eviction (max 100 cursors)
|
||||
- **Grep/Regex Filtering:** Added `grep` and `grep_ignorecase` parameters to filter results with regex patterns before pagination.
|
||||
- **Bypass Option:** Added `return_all` parameter to retrieve complete datasets (with large response warnings).
|
||||
- **Cursor Management Tools:** New MCP tools for cursor lifecycle management:
|
||||
- `cursor_next(cursor_id)` - Fetch next page of results
|
||||
- `cursor_list()` - List active cursors for current session
|
||||
- `cursor_delete(cursor_id)` - Delete specific cursor
|
||||
- `cursor_delete_all()` - Delete all session cursors
|
||||
- **Enumeration Resources:** New lightweight MCP resources for quick data enumeration (more efficient than tool calls):
|
||||
- `ghidra://instances` - List all active Ghidra instances
|
||||
- `ghidra://instance/{port}/summary` - Program overview with statistics
|
||||
- `ghidra://instance/{port}/functions` - List functions (capped at 1000)
|
||||
- `ghidra://instance/{port}/strings` - List strings (capped at 500)
|
||||
- `ghidra://instance/{port}/data` - List data items (capped at 1000)
|
||||
- `ghidra://instance/{port}/structs` - List struct types (capped at 500)
|
||||
- `ghidra://instance/{port}/xrefs/to/{address}` - Cross-references to an address
|
||||
- `ghidra://instance/{port}/xrefs/from/{address}` - Cross-references from an address
|
||||
|
||||
### Changed
|
||||
- **MCP Dependency Upgrade:** Updated from `mcp==1.6.0` to `mcp>=1.22.0` for FastMCP Context support.
|
||||
- **Version Strategy:** Switched to date-based versioning (YYYY.MM.D format).
|
||||
- **Tool Updates:** 11 tools now support pagination with grep filtering:
|
||||
- `functions_list` - List functions with pagination
|
||||
- `functions_decompile` - Decompiled code with line pagination (grep for code patterns)
|
||||
- `functions_disassemble` - Assembly with instruction pagination (grep for opcodes)
|
||||
- `functions_get_variables` - Function variables with pagination
|
||||
- `data_list` - List data items with pagination
|
||||
- `data_list_strings` - List strings with pagination
|
||||
- `xrefs_list` - List cross-references with pagination
|
||||
- `structs_list` - List struct types with pagination
|
||||
- `structs_get` - Struct fields with pagination (grep for field names/types)
|
||||
- `analysis_get_callgraph` - Call graph edges with pagination
|
||||
- `analysis_get_dataflow` - Data flow steps with pagination
|
||||
- **LLM-Friendly Responses:** Added prominent `_message` field to guide LLMs on cursor continuation.
|
||||
|
||||
### Fixed
|
||||
- **FastMCP Compatibility:** Removed deprecated `version` parameter from FastMCP constructor.
|
||||
|
||||
### Security
|
||||
- **ReDoS Protection:** Added validation for grep regex patterns to prevent catastrophic backtracking attacks.
|
||||
- Pattern length limit (500 chars)
|
||||
- Repetition operator limit (15 max)
|
||||
- Detection of dangerous nested quantifier patterns like `(a+)+`
|
||||
- **Session Spoofing Prevention:** Removed user-controllable `session_id` parameter from all tools.
|
||||
- Sessions now derived from FastMCP context (`ctx.session`, `ctx.client_id`)
|
||||
- Prevents users from accessing or manipulating other sessions' cursors
|
||||
- **Recursion Depth Limit:** Added depth limit (10) to grep matching to prevent stack overflow on deeply nested data.
|
||||
|
||||
## [2.0.0] - 2025-11-11
|
||||
|
||||
### Added
|
||||
- **MCP Integration Refactor:** Refactored the Python bridge for improved MCP integration. (337f89e)
|
||||
- Introduced MCP resources for loading context (e.g., instances, functions, disassembly).
|
||||
- Added namespaced tools (e.g., `instance.*`, `function.*`, `data.*`) for better organization and discoverability.
|
||||
- Implemented a "current working instance" concept to simplify commands by implicitly targeting the active Ghidra instance.
|
||||
- **Analysis Prompts:** Added pre-defined prompts for common analysis tasks, including `reverse_engineer_binary` for comprehensive analysis. (337f89e, 3134581)
|
||||
- **String Data Listing:** Added a new endpoint to list string data in the binary, with pagination and filtering by content. Python bridge support via `list_strings()` function. (f71f4aa)
|
||||
- **Comprehensive Data Manipulation:** Added tools/endpoints for creating (`create_data`), deleting (`delete_data`), renaming (`rename_data`), changing type (`set_data_type`), and combined updates (`update_data`) for data items. Supports common types (byte, word, dword, string, etc.). (6c28553, 5797fb3, 28870e9)
|
||||
- **Enhanced Cross-Reference (Xrefs) Analysis:** Implemented accurate xref tools (`get_references_to`, `get_references_from`) using Ghidra's ReferenceManager. Features include detailed info, bi-directional search, type filtering, and simplified bridge output. (96788f3)
|
||||
- **Memory Operations:** Added tools/endpoints for reading (`read_memory`) and writing (`write_memory`) to program memory. (454c739)
|
||||
@ -15,6 +156,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- **Enhanced Decompiler Controls:** Added options for raw vs. clean pseudocode output and multiple simplification styles. (454c739)
|
||||
|
||||
### Changed
|
||||
- **Bridge Refactor & Namespacing:** Reorganized bridge tools into namespaces (e.g., `instance.list_instances`, `function.get_function_details`) as part of the MCP integration refactor. (337f89e)
|
||||
- **Breaking: HATEOAS API v2 & Bridge Update:** Migrated fully to a HATEOAS-driven API (v2). The Python bridge (`bridge_mcp_hydra.py`) now *exclusively* uses this API, removing legacy support. Responses are simplified for AI agents, including text representations for structured data (e.g., disassembly). All endpoints require HATEOAS compliance (e.g., `_links`). (4bc2267, 4f3042f)
|
||||
- **Optimized Variable Listing:** Improved performance of the `/variables` endpoint with efficient pagination and a `globalOnly` filter. (6c865c4)
|
||||
- **Standardized Responses:** Unified all endpoints to use structured JSON and standardized HATEOAS links. (454c739, 4bc2267)
|
||||
@ -26,6 +168,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- **Ghidra 11+ Compatibility:** Resolved various API compatibility issues, particularly for cross-references (`XrefsEndpoints`). (5dc59ce, 2b1fe6c, 0eaa19a, 9443101)
|
||||
- **Data Operations:** Fixed issues with HTTP request body consumption, parameter naming (`type` vs `dataType`), and name preservation during type changes. (28870e9)
|
||||
- **Function Commenting:** Corrected `set_decompiler_comment` to apply comments at the function level. (2a1607c)
|
||||
- **Call Graph Parameter Handling:** Updated the CallGraph endpoint to properly accept both function name and address parameters for flexibility. (fa8cc64)
|
||||
- **Endpoint Functionality:** Addressed various issues including endpoint registration, handling of program-dependent endpoints, URL encoding, transaction management, and inconsistent response formats. (various commits, e.g., 4bc2267)
|
||||
|
||||
## [1.4.0] - 2025-04-08
|
||||
@ -96,7 +239,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
## [1.1] - 2025-03-30
|
||||
|
||||
### Added
|
||||
- Initial release of GhydraMCP bridge
|
||||
- Initial release of MCGhidra bridge
|
||||
- Basic Ghidra instance management tools
|
||||
- Function analysis tools
|
||||
- Variable manipulation tools
|
||||
@ -107,9 +250,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- Initial project setup
|
||||
- Basic MCP bridge functionality
|
||||
|
||||
[unreleased]: https://github.com/teal-bauer/GhydraMCP/compare/v1.4.0...HEAD
|
||||
[1.4.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.3.0...v1.4.0
|
||||
[1.3.0]: https://github.com/teal-bauer/GhydraMCP/compare/v1.2...v1.3.0
|
||||
[1.2]: https://github.com/teal-bauer/GhydraMCP/compare/v1.1...v1.2
|
||||
[1.1]: https://github.com/teal-bauer/GhydraMCP/compare/1.0...v1.1
|
||||
[1.0]: https://github.com/teal-bauer/GhydraMCP/releases/tag/1.0
|
||||
[unreleased]: https://github.com/teal-bauer/MCGhidra/compare/v2025.12.1...HEAD
|
||||
[2025.12.1]: https://github.com/teal-bauer/MCGhidra/compare/v2.0.0...v2025.12.1
|
||||
[2.0.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.4.0...v2.0.0
|
||||
[1.4.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.3.0...v1.4.0
|
||||
[1.3.0]: https://github.com/teal-bauer/MCGhidra/compare/v1.2...v1.3.0
|
||||
[1.2]: https://github.com/teal-bauer/MCGhidra/compare/v1.1...v1.2
|
||||
[1.1]: https://github.com/teal-bauer/MCGhidra/compare/1.0...v1.1
|
||||
[1.0]: https://github.com/teal-bauer/MCGhidra/releases/tag/1.0
|
||||
247
CONTRIBUTING.md
Normal file
247
CONTRIBUTING.md
Normal file
@ -0,0 +1,247 @@
|
||||
# Contributing to MCGhidra
|
||||
|
||||
Thank you for your interest in contributing to MCGhidra! This document provides guidelines and information for contributors.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Project Structure](#project-structure)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Versioning](#versioning)
|
||||
- [Code Standards](#code-standards)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Project Structure
|
||||
|
||||
MCGhidra consists of two main components:
|
||||
|
||||
1. **Java Plugin for Ghidra** (`src/main/java/eu/starsong/ghidra/`):
|
||||
- Main class: `MCGhidraPlugin.java`
|
||||
- API constants: `api/ApiConstants.java`
|
||||
- Endpoints: `endpoints/` directory
|
||||
- Data models: `model/` directory
|
||||
- Utilities: `util/` directory
|
||||
|
||||
2. **Python MCP Bridge** (`bridge_mcp_hydra.py`):
|
||||
- Implements the Model Context Protocol (MCP)
|
||||
- Connects AI assistants to the Ghidra plugin via HTTP
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 21 (required for plugin development)
|
||||
- Maven 3.8+
|
||||
- Python 3.11+
|
||||
- Ghidra (latest stable version recommended)
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/starsong-consulting/MCGhidra.git
|
||||
cd MCGhidra
|
||||
|
||||
# Build the project
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `target/MCGhidra-[version].zip` - The Ghidra plugin only
|
||||
- `target/MCGhidra-Complete-[version].zip` - Complete package with plugin and bridge script
|
||||
|
||||
### Installing for Development
|
||||
|
||||
1. Build the project as described above
|
||||
2. In Ghidra, go to `File` -> `Install Extensions`
|
||||
3. Click the `+` button
|
||||
4. Select the `MCGhidra-[version].zip` file
|
||||
5. Restart Ghidra
|
||||
6. Enable the plugin in `File` -> `Configure` -> `Developer`
|
||||
|
||||
### Python Bridge Setup
|
||||
|
||||
Install the required Python dependencies:
|
||||
|
||||
```bash
|
||||
pip install mcp==1.6.0 requests==2.32.3
|
||||
```
|
||||
|
||||
Or use uv:
|
||||
|
||||
```bash
|
||||
uv pip install mcp==1.6.0 requests==2.32.3
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
MCGhidra follows semantic versioning (SemVer) and uses explicit API versions:
|
||||
|
||||
### Version Numbers
|
||||
|
||||
When making changes, update version numbers in these locations:
|
||||
|
||||
1. **Plugin Version** in `src/main/java/eu/starsong/ghidra/api/ApiConstants.java`:
|
||||
```java
|
||||
public static final String PLUGIN_VERSION = "v2.0.0";
|
||||
```
|
||||
|
||||
2. **Bridge Version** in `bridge_mcp_hydra.py`:
|
||||
```python
|
||||
BRIDGE_VERSION = "v2.0.0"
|
||||
```
|
||||
|
||||
### API Versions
|
||||
|
||||
The API version is tracked separately from the implementation version:
|
||||
|
||||
1. **API Version** in `src/main/java/eu/starsong/ghidra/api/ApiConstants.java`:
|
||||
```java
|
||||
public static final int API_VERSION = 2;
|
||||
```
|
||||
|
||||
2. **Required API Version** in `bridge_mcp_hydra.py`:
|
||||
```python
|
||||
REQUIRED_API_VERSION = 2
|
||||
```
|
||||
|
||||
### When to Update Versions
|
||||
|
||||
- **Patch Version** (`x.y.Z`): Bug fixes and minor changes that don't affect API compatibility
|
||||
- **Minor Version** (`x.Y.z`): New features that are backward compatible
|
||||
- **Major Version** (`X.y.z`): Breaking changes that aren't backward compatible
|
||||
- **API Version**: Only increment when making incompatible API changes
|
||||
|
||||
### Important Versioning Rules
|
||||
|
||||
1. **Bridge Modifications**: When modifying the MCP bridge script (`bridge_mcp_hydra.py`), update the `BRIDGE_VERSION` string but only update the `REQUIRED_API_VERSION` if the changes require API compatibility changes.
|
||||
|
||||
2. **Java Plugin Modifications**: When making changes to the Java plugin:
|
||||
- Update `PLUGIN_VERSION` string for all changes
|
||||
- Only increment `API_VERSION` when introducing breaking changes to the API
|
||||
|
||||
3. **API Compatibility**: The bridge script and Java plugin must have matching API versions to work together. The bridge verifies this at runtime.
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Java Code Standards
|
||||
|
||||
- Follow Java naming conventions
|
||||
- Add comprehensive JavaDoc comments for public methods
|
||||
- Ensure proper exception handling
|
||||
- Follow Ghidra extension development best practices
|
||||
- Follow HATEOAS principles for API endpoints
|
||||
- Implement proper null checks and input validation
|
||||
|
||||
### Python Code Standards
|
||||
|
||||
- Follow PEP 8 style guidelines
|
||||
- Add docstrings for all functions
|
||||
- Use type hints for function parameters and return types
|
||||
- Implement proper error handling
|
||||
- Ensure thread-safety for multi-threaded operations
|
||||
|
||||
### Commit Message Standards
|
||||
|
||||
Follow the conventional commits format:
|
||||
|
||||
```
|
||||
<type>: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
Types:
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Code style changes (formatting, etc.)
|
||||
- `refactor`: Code refactoring without functionality changes
|
||||
- `perf`: Performance improvements
|
||||
- `test`: Adding or updating tests
|
||||
- `build`: Build system changes
|
||||
- `ci`: CI configuration changes
|
||||
- `chore`: Other changes that don't modify src or test files
|
||||
|
||||
Example:
|
||||
```
|
||||
feat: Add support for string listing in binary files
|
||||
|
||||
This adds a new endpoint to list all defined strings in the binary
|
||||
with pagination and filtering by content.
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Branch Naming Convention**:
|
||||
- Features: `feature/short-description`
|
||||
- Fixes: `fix/issue-description`
|
||||
- Documentation: `docs/description`
|
||||
|
||||
2. **Before Creating a PR**:
|
||||
- Ensure all tests pass
|
||||
- Update documentation if needed
|
||||
- Update version numbers if needed
|
||||
- Add appropriate entries to CHANGELOG.md
|
||||
|
||||
3. **PR Template**:
|
||||
- Clearly describe the changes
|
||||
- Reference any related issues
|
||||
- Include any special testing instructions
|
||||
- List any breaking changes
|
||||
|
||||
4. **Review Process**:
|
||||
- At least one core contributor must review and approve
|
||||
- Address all requested changes
|
||||
- Ensure CI checks pass
|
||||
|
||||
## Release Process
|
||||
|
||||
1. **Preparation**:
|
||||
- Ensure all tests pass
|
||||
- Update version numbers in:
|
||||
- `src/main/java/eu/starsong/ghidra/api/ApiConstants.java`
|
||||
- `bridge_mcp_hydra.py`
|
||||
- Update CHANGELOG.md with release notes
|
||||
- Move content from "Unreleased" section to a new release section
|
||||
- Merge all changes to the main branch
|
||||
|
||||
2. **Creating a Release**:
|
||||
- Tag the release commit with the version number (e.g., `v2.0.0`)
|
||||
- Push the tag to GitHub
|
||||
- The GitHub Actions workflow will automatically:
|
||||
- Build the release artifacts
|
||||
- Create a GitHub release
|
||||
- Upload the artifacts
|
||||
|
||||
3. **Post-Release**:
|
||||
- Update version numbers to next development version
|
||||
- Create a new "Unreleased" section in CHANGELOG.md
|
||||
- Announce the release in appropriate channels
|
||||
|
||||
## Testing
|
||||
|
||||
Please include appropriate tests for your changes:
|
||||
|
||||
1. **HTTP API Tests**: For Java plugin endpoint changes
|
||||
2. **MCP Bridge Tests**: For Python bridge functionality
|
||||
|
||||
Run the tests with:
|
||||
```bash
|
||||
python run_tests.py
|
||||
```
|
||||
|
||||
See TESTING.md for more detailed information on testing procedures.
|
||||
|
||||
## Questions and Support
|
||||
|
||||
If you have questions or need help, please:
|
||||
1. Open an issue on GitHub
|
||||
2. Check existing documentation
|
||||
3. Reach out to the maintainers directly
|
||||
|
||||
Thank you for contributing to MCGhidra!
|
||||
@ -1,4 +1,4 @@
|
||||
# GhydraMCP Ghidra Plugin HTTP API v2
|
||||
# MCGhidra Ghidra Plugin HTTP API v2
|
||||
|
||||
## Overview
|
||||
|
||||
@ -159,7 +159,7 @@ Returns information about the current plugin instance, including details about t
|
||||
```
|
||||
|
||||
### `GET /instances`
|
||||
Returns information about all active GhydraMCP plugin instances.
|
||||
Returns information about all active MCGhidra plugin instances.
|
||||
```json
|
||||
{
|
||||
"id": "req-instances",
|
||||
@ -371,6 +371,234 @@ Represents defined data items in memory.
|
||||
- **`PATCH /data/{address}`**: Modify a data item (e.g., change `name`, `type`, `comment`). Payload specifies changes.
|
||||
- **`DELETE /data/{address}`**: Undefine the data item at the specified address.
|
||||
|
||||
### 6.1 Strings
|
||||
|
||||
Provides access to string data in the binary.
|
||||
|
||||
- **`GET /strings`**: List all defined strings in the binary. Supports pagination and filtering.
|
||||
- Query Parameters:
|
||||
- `?offset=[int]`: Number of strings to skip (default: 0).
|
||||
- `?limit=[int]`: Maximum number of strings to return (default: 2000).
|
||||
- `?filter=[string]`: Only include strings containing this substring (case-insensitive).
|
||||
```json
|
||||
// Example Response
|
||||
"result": [
|
||||
{
|
||||
"address": "0x00401234",
|
||||
"value": "Hello, world!",
|
||||
"length": 14,
|
||||
"type": "string",
|
||||
"name": "aHelloWorld"
|
||||
},
|
||||
{
|
||||
"address": "0x00401250",
|
||||
"value": "Error: could not open file",
|
||||
"length": 26,
|
||||
"type": "string",
|
||||
"name": "aErrorCouldNotO"
|
||||
}
|
||||
],
|
||||
"_links": {
|
||||
"self": { "href": "/strings?offset=0&limit=10" },
|
||||
"next": { "href": "/strings?offset=10&limit=10" }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Structs
|
||||
|
||||
Provides functionality for creating and managing struct (composite) data types.
|
||||
|
||||
- **`GET /structs`**: List all struct data types in the program. Supports pagination and filtering.
|
||||
- Query Parameters:
|
||||
- `?offset=[int]`: Number of structs to skip (default: 0).
|
||||
- `?limit=[int]`: Maximum number of structs to return (default: 100).
|
||||
- `?category=[string]`: Filter by category path (e.g. "/winapi").
|
||||
```json
|
||||
// Example Response
|
||||
"result": [
|
||||
{
|
||||
"name": "MyStruct",
|
||||
"path": "/custom/MyStruct",
|
||||
"size": 16,
|
||||
"numFields": 4,
|
||||
"category": "/custom",
|
||||
"description": "Custom data structure"
|
||||
},
|
||||
{
|
||||
"name": "FileHeader",
|
||||
"path": "/FileHeader",
|
||||
"size": 32,
|
||||
"numFields": 8,
|
||||
"category": "/",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"_links": {
|
||||
"self": { "href": "/structs?offset=0&limit=100" },
|
||||
"program": { "href": "/program" }
|
||||
}
|
||||
```
|
||||
|
||||
- **`GET /structs?name={struct_name}`**: Get detailed information about a specific struct including all fields.
|
||||
```json
|
||||
// Example Response for GET /structs?name=MyStruct
|
||||
"result": {
|
||||
"name": "MyStruct",
|
||||
"path": "/custom/MyStruct",
|
||||
"size": 16,
|
||||
"category": "/custom",
|
||||
"description": "Custom data structure",
|
||||
"numFields": 4,
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"offset": 0,
|
||||
"length": 4,
|
||||
"type": "int",
|
||||
"typePath": "/int",
|
||||
"comment": "Unique identifier"
|
||||
},
|
||||
{
|
||||
"name": "flags",
|
||||
"offset": 4,
|
||||
"length": 4,
|
||||
"type": "dword",
|
||||
"typePath": "/dword",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"name": "data_ptr",
|
||||
"offset": 8,
|
||||
"length": 4,
|
||||
"type": "pointer",
|
||||
"typePath": "/pointer",
|
||||
"comment": "Pointer to data"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"offset": 12,
|
||||
"length": 4,
|
||||
"type": "uint",
|
||||
"typePath": "/uint",
|
||||
"comment": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"_links": {
|
||||
"self": { "href": "/structs?name=MyStruct" },
|
||||
"structs": { "href": "/structs" },
|
||||
"program": { "href": "/program" }
|
||||
}
|
||||
```
|
||||
|
||||
- **`POST /structs/create`**: Create a new struct data type.
|
||||
- Request Payload:
|
||||
- `name`: Name for the new struct (required).
|
||||
- `category`: Category path (optional, defaults to root).
|
||||
- `description`: Description for the struct (optional).
|
||||
```json
|
||||
// Example Request Payload
|
||||
{
|
||||
"name": "NetworkPacket",
|
||||
"category": "/network",
|
||||
"description": "Network packet structure"
|
||||
}
|
||||
|
||||
// Example Response
|
||||
"result": {
|
||||
"name": "NetworkPacket",
|
||||
"path": "/network/NetworkPacket",
|
||||
"category": "/network",
|
||||
"size": 0,
|
||||
"message": "Struct created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
- **`POST /structs/addfield`**: Add a field to an existing struct.
|
||||
- Request Payload:
|
||||
- `struct`: Name of the struct to modify (required).
|
||||
- `fieldName`: Name for the new field (required).
|
||||
- `fieldType`: Data type for the field (required, e.g. "int", "char", "pointer").
|
||||
- `offset`: Specific offset to insert field (optional, appends to end if not specified).
|
||||
- `comment`: Comment for the field (optional).
|
||||
```json
|
||||
// Example Request Payload
|
||||
{
|
||||
"struct": "NetworkPacket",
|
||||
"fieldName": "header",
|
||||
"fieldType": "dword",
|
||||
"comment": "Packet header"
|
||||
}
|
||||
|
||||
// Example Response
|
||||
"result": {
|
||||
"struct": "NetworkPacket",
|
||||
"fieldName": "header",
|
||||
"fieldType": "dword",
|
||||
"offset": 0,
|
||||
"length": 4,
|
||||
"structSize": 4,
|
||||
"message": "Field added successfully"
|
||||
}
|
||||
```
|
||||
|
||||
- **`POST /structs/updatefield`**: Update an existing field in a struct (rename, change type, or modify comment).
|
||||
- Request Payload:
|
||||
- `struct`: Name of the struct to modify (required).
|
||||
- `fieldOffset` OR `fieldName`: Identify the field to update (one required).
|
||||
- `newName`: New name for the field (optional).
|
||||
- `newType`: New data type for the field (optional).
|
||||
- `newComment`: New comment for the field (optional).
|
||||
- At least one of `newName`, `newType`, or `newComment` must be provided.
|
||||
```json
|
||||
// Example Request Payload - rename a field
|
||||
{
|
||||
"struct": "NetworkPacket",
|
||||
"fieldName": "header",
|
||||
"newName": "packet_header",
|
||||
"newComment": "Updated packet header field"
|
||||
}
|
||||
|
||||
// Example Request Payload - change type by offset
|
||||
{
|
||||
"struct": "NetworkPacket",
|
||||
"fieldOffset": 0,
|
||||
"newType": "qword"
|
||||
}
|
||||
|
||||
// Example Response
|
||||
"result": {
|
||||
"struct": "NetworkPacket",
|
||||
"offset": 0,
|
||||
"originalName": "header",
|
||||
"originalType": "dword",
|
||||
"originalComment": "Packet header",
|
||||
"newName": "packet_header",
|
||||
"newType": "dword",
|
||||
"newComment": "Updated packet header field",
|
||||
"length": 4,
|
||||
"message": "Field updated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
- **`POST /structs/delete`**: Delete a struct data type.
|
||||
- Request Payload:
|
||||
- `name`: Name of the struct to delete (required).
|
||||
```json
|
||||
// Example Request Payload
|
||||
{
|
||||
"name": "NetworkPacket"
|
||||
}
|
||||
|
||||
// Example Response
|
||||
"result": {
|
||||
"name": "NetworkPacket",
|
||||
"path": "/network/NetworkPacket",
|
||||
"category": "/network",
|
||||
"message": "Struct deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Memory Segments
|
||||
|
||||
Represents memory blocks/sections defined in the program.
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
# GhydraMCP Bridge API Documentation
|
||||
|
||||
## Overview
|
||||
This document describes the MCP tools and resources exposed by the GhydraMCP bridge that connects to Ghidra's HTTP API. The bridge provides a higher-level interface optimized for AI agent usage.
|
||||
|
||||
## Core Concepts
|
||||
- Each Ghidra instance runs its own HTTP server (default port 8192)
|
||||
- The bridge discovers and manages multiple Ghidra instances
|
||||
- Programs are addressed by their unique identifier within Ghidra (`project:/path/to/file`).
|
||||
- The primary identifier for a program is its Ghidra path, e.g., `myproject:/path/to/mybinary.exe`.
|
||||
- The bridge must keep track of which plugin host and port has which project & file and route accordingly
|
||||
- Tools are organized by resource type (programs, functions, data, etc.)
|
||||
- Consistent response format with success/error indicators
|
||||
|
||||
## Instance Management Tools
|
||||
|
||||
### `list_instances`
|
||||
List all active Ghidra instances with their ports and project info.
|
||||
|
||||
### `discover_instances`
|
||||
Scan for available Ghidra instances by port range.
|
||||
|
||||
### `register_instance`
|
||||
Manually register a Ghidra instance by port/URL.
|
||||
|
||||
## Program Analysis Tools
|
||||
|
||||
### `list_functions`
|
||||
List functions in current program with pagination.
|
||||
|
||||
### `get_function`
|
||||
Get details and decompilation for a function by name.
|
||||
|
||||
### `get_function_by_address`
|
||||
Get function details by memory address.
|
||||
|
||||
### `decompile_function_by_address`
|
||||
Decompile function at specific address.
|
||||
|
||||
### `list_segments`
|
||||
List memory segments/sections in program.
|
||||
|
||||
### `list_data_items`
|
||||
List defined data items in program.
|
||||
|
||||
### `read_memory`
|
||||
Read bytes from memory at address. Parameters:
|
||||
- `address`: Hex address
|
||||
- `length`: Bytes to read
|
||||
- `format`: "hex", "base64" or "string" output format
|
||||
|
||||
### `write_memory`
|
||||
Write bytes to memory at address (use with caution). Parameters:
|
||||
- `address`: Hex address
|
||||
- `bytes`: Data to write
|
||||
- `format`: "hex", "base64" or "string" input format
|
||||
|
||||
### `list_variables`
|
||||
List global variables with search/filter.
|
||||
|
||||
## Modification Tools
|
||||
|
||||
### `update_function`
|
||||
Rename a function.
|
||||
|
||||
### `update_data`
|
||||
Rename data at memory address.
|
||||
|
||||
### `set_function_prototype`
|
||||
Change a function's signature.
|
||||
|
||||
### `rename_local_variable`
|
||||
Rename variable within function.
|
||||
|
||||
### `set_local_variable_type`
|
||||
Change variable's data type.
|
||||
|
||||
## Response Format
|
||||
All tools return responses in this format:
|
||||
```json
|
||||
{
|
||||
"id": "request-id",
|
||||
"instance": "http://host:port",
|
||||
"success": true/false,
|
||||
"result": {...}, // Tool-specific data
|
||||
"error": { // Only on failure
|
||||
"code": "...",
|
||||
"message": "..."
|
||||
},
|
||||
"_links": { // HATEOAS links
|
||||
"self": {"href": "/path"},
|
||||
"related": {"href": "/other"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
1. Discover available instances:
|
||||
```python
|
||||
discover_instances()
|
||||
```
|
||||
|
||||
2. List functions in first instance:
|
||||
```python
|
||||
list_functions(port=8192, limit=10)
|
||||
```
|
||||
|
||||
3. Decompile main function:
|
||||
```python
|
||||
get_function(port=8192, name="main")
|
||||
```
|
||||
|
||||
4. Rename a function:
|
||||
```python
|
||||
update_function(port=8192, name="FUN_1234", new_name="parse_data")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
- Check `success` field first
|
||||
- On failure, `error` contains details
|
||||
- Common error codes:
|
||||
- `INSTANCE_NOT_FOUND`
|
||||
- `RESOURCE_NOT_FOUND`
|
||||
- `INVALID_PARAMETER`
|
||||
- `TRANSACTION_FAILED`
|
||||
|
||||
## Advanced Analysis Tools
|
||||
|
||||
### `list_xrefs`
|
||||
List cross-references between code/data. Parameters:
|
||||
- `to_addr`: Filter refs to this address
|
||||
- `from_addr`: Filter refs from this address
|
||||
- `type`: Filter by ref type ("CALL", "READ", etc)
|
||||
- Basic pagination via `offset`/`limit`
|
||||
|
||||
### `analyze_program`
|
||||
Run Ghidra analysis with optional settings:
|
||||
- `analysis_options`: Dict of analysis passes to enable
|
||||
|
||||
### `get_callgraph`
|
||||
Get function call graph visualization data:
|
||||
- `function`: Starting function (defaults to entry point)
|
||||
- `max_depth`: Maximum call depth (default: 3)
|
||||
|
||||
### `get_dataflow`
|
||||
Perform data flow analysis from address:
|
||||
- `address`: Starting point in hex
|
||||
- `direction`: "forward" or "backward"
|
||||
- `max_steps`: Max analysis steps
|
||||
171
Makefile
Normal file
171
Makefile
Normal file
@ -0,0 +1,171 @@
|
||||
# MCGhidra Makefile
|
||||
# Convenient commands for Docker and development operations
|
||||
|
||||
.PHONY: help build build-dev up up-dev down down-dev logs logs-dev \
|
||||
shell status clean analyze test health
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "MCGhidra Docker Management"
|
||||
@echo "============================"
|
||||
@echo ""
|
||||
@echo "Build commands:"
|
||||
@echo " make build Build production Docker image"
|
||||
@echo " make build-dev Build development Docker image"
|
||||
@echo " make build-all Build both images"
|
||||
@echo ""
|
||||
@echo "Run commands:"
|
||||
@echo " make up Start production container"
|
||||
@echo " make up-dev Start development container"
|
||||
@echo " make down Stop production container"
|
||||
@echo " make down-dev Stop development container"
|
||||
@echo " make down-all Stop all containers"
|
||||
@echo ""
|
||||
@echo "Analysis commands:"
|
||||
@echo " make analyze FILE=path/to/binary Analyze a binary"
|
||||
@echo " make analyze-bg FILE=path/to/binary Analyze in background"
|
||||
@echo ""
|
||||
@echo "Utility commands:"
|
||||
@echo " make shell Start interactive shell in container"
|
||||
@echo " make logs View production container logs"
|
||||
@echo " make logs-dev View development container logs"
|
||||
@echo " make status Check container status"
|
||||
@echo " make health Check API health"
|
||||
@echo " make clean Remove containers and volumes"
|
||||
@echo " make clean-all Remove everything including images"
|
||||
@echo ""
|
||||
@echo "MCP Server commands:"
|
||||
@echo " make mcp Start the MCP server (Python)"
|
||||
@echo " make mcp-dev Start MCP server in development mode"
|
||||
@echo ""
|
||||
|
||||
# =============================================================================
|
||||
# Build Commands
|
||||
# =============================================================================
|
||||
|
||||
build:
|
||||
docker compose build mcghidra
|
||||
|
||||
build-dev:
|
||||
docker compose build mcghidra-dev
|
||||
|
||||
build-all: build build-dev
|
||||
|
||||
# =============================================================================
|
||||
# Run Commands
|
||||
# =============================================================================
|
||||
|
||||
up:
|
||||
docker compose --profile prod up -d mcghidra
|
||||
@echo "MCGhidra starting... checking health in 30 seconds"
|
||||
@sleep 30
|
||||
@$(MAKE) health || echo "Server may still be starting up..."
|
||||
|
||||
up-dev:
|
||||
docker compose --profile dev up -d mcghidra-dev
|
||||
@echo "MCGhidra (dev) starting..."
|
||||
|
||||
down:
|
||||
docker compose --profile prod down
|
||||
|
||||
down-dev:
|
||||
docker compose --profile dev down
|
||||
|
||||
down-all:
|
||||
docker compose --profile prod --profile dev --profile debug down
|
||||
|
||||
restart: down up
|
||||
|
||||
restart-dev: down-dev up-dev
|
||||
|
||||
# =============================================================================
|
||||
# Analysis Commands
|
||||
# =============================================================================
|
||||
|
||||
# Analyze a binary file
|
||||
# Usage: make analyze FILE=/path/to/binary
|
||||
analyze:
|
||||
ifndef FILE
|
||||
@echo "Error: FILE is required. Usage: make analyze FILE=/path/to/binary"
|
||||
@exit 1
|
||||
endif
|
||||
@echo "Analyzing: $(FILE)"
|
||||
docker compose run --rm -v "$(dir $(FILE)):/binaries:ro" mcghidra /binaries/$(notdir $(FILE))
|
||||
|
||||
# Analyze in background (detached)
|
||||
analyze-bg:
|
||||
ifndef FILE
|
||||
@echo "Error: FILE is required. Usage: make analyze-bg FILE=/path/to/binary"
|
||||
@exit 1
|
||||
endif
|
||||
@echo "Starting background analysis of: $(FILE)"
|
||||
docker compose run -d -v "$(dir $(FILE)):/binaries:ro" mcghidra /binaries/$(notdir $(FILE))
|
||||
|
||||
# =============================================================================
|
||||
# Utility Commands
|
||||
# =============================================================================
|
||||
|
||||
shell:
|
||||
docker compose --profile debug run --rm mcghidra-shell
|
||||
|
||||
logs:
|
||||
docker compose logs -f mcghidra
|
||||
|
||||
logs-dev:
|
||||
docker compose logs -f mcghidra-dev
|
||||
|
||||
status:
|
||||
@echo "=== Container Status ==="
|
||||
@docker compose ps -a
|
||||
@echo ""
|
||||
@echo "=== Resource Usage ==="
|
||||
@docker stats --no-stream $$(docker compose ps -q 2>/dev/null) 2>/dev/null || echo "No containers running"
|
||||
|
||||
health:
|
||||
@echo "Checking MCGhidra API health..."
|
||||
@curl -sf http://localhost:$${MCGHIDRA_PORT:-8192}/ | python3 -m json.tool 2>/dev/null \
|
||||
|| echo "API not responding (server may be starting or binary being analyzed)"
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup Commands
|
||||
# =============================================================================
|
||||
|
||||
clean:
|
||||
docker compose --profile prod --profile dev --profile debug down -v
|
||||
@echo "Containers and volumes removed"
|
||||
|
||||
clean-all: clean
|
||||
docker rmi mcghidra:latest mcghidra:dev 2>/dev/null || true
|
||||
@echo "Images removed"
|
||||
|
||||
prune:
|
||||
docker system prune -f
|
||||
@echo "Docker system pruned"
|
||||
|
||||
# =============================================================================
|
||||
# MCP Server Commands
|
||||
# =============================================================================
|
||||
|
||||
mcp:
|
||||
uv run python -m mcghidra
|
||||
|
||||
mcp-dev:
|
||||
uv run python -m mcghidra --verbose
|
||||
|
||||
# =============================================================================
|
||||
# Development Commands
|
||||
# =============================================================================
|
||||
|
||||
test:
|
||||
uv run pytest tests/ -v
|
||||
|
||||
lint:
|
||||
uv run ruff check src/
|
||||
|
||||
format:
|
||||
uv run ruff format src/
|
||||
|
||||
# Check if binaries directory exists
|
||||
check-binaries:
|
||||
@mkdir -p binaries
|
||||
@echo "Binaries directory ready at ./binaries/"
|
||||
328
QUICKSTART.md
Normal file
328
QUICKSTART.md
Normal file
@ -0,0 +1,328 @@
|
||||
# MCGhidra Quick Start Guide
|
||||
|
||||
## What is MCGhidra?
|
||||
|
||||
MCGhidra is a complete reverse engineering platform that combines:
|
||||
- **Ghidra** - NSA's powerful binary analysis tool
|
||||
- **Docker** - Containerized, reproducible analysis environment
|
||||
- **HTTP REST API** - HATEOAS-compliant REST interface
|
||||
- **MCP Server** - FastMCP-based Model Context Protocol integration
|
||||
- **ARM Firmware Support** - Tools for analyzing raw embedded firmware
|
||||
|
||||
## 5-Minute Quick Start
|
||||
|
||||
### 1. Analyze a Standard Binary (ELF/PE/Mach-O)
|
||||
|
||||
```bash
|
||||
cd /home/rpm/claude/mcghidra/MCGhidra
|
||||
|
||||
# Build the Docker image (one time)
|
||||
docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
|
||||
# Analyze any standard binary
|
||||
docker run -d --name my-analysis \
|
||||
-p 8192:8192 \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
mcghidra:latest \
|
||||
/binaries/your-binary
|
||||
|
||||
# Wait ~20 seconds for analysis, then access HTTP API
|
||||
curl http://localhost:8192/
|
||||
curl http://localhost:8192/functions | jq '.functions[] | {name, address}'
|
||||
curl http://localhost:8192/functions/<address>/decompile
|
||||
```
|
||||
|
||||
### 2. Analyze ARM Firmware (Raw Binary)
|
||||
|
||||
```bash
|
||||
# Step 1: Create ELF wrapper
|
||||
python3 docker/arm_firmware_prep.py \
|
||||
your-firmware.bin \
|
||||
binaries/your-firmware.elf \
|
||||
0x00000000
|
||||
|
||||
# Step 2: Analyze normally
|
||||
docker run -d --name arm-firmware \
|
||||
-p 8192:8192 \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
mcghidra:latest \
|
||||
/binaries/your-firmware.elf
|
||||
```
|
||||
|
||||
### 3. Use the MCP Server
|
||||
|
||||
```bash
|
||||
# The MCP server is located at:
|
||||
cd /home/rpm/claude/mcghidra/MCGhidra
|
||||
./launch.sh
|
||||
|
||||
# Or with uv:
|
||||
cd MCGhidra && uv run mcghidra
|
||||
```
|
||||
|
||||
## HTTP API Overview
|
||||
|
||||
Once analysis completes, the API is available at `http://localhost:8192/`:
|
||||
|
||||
### Core Endpoints
|
||||
|
||||
```bash
|
||||
# Program information
|
||||
GET /program
|
||||
|
||||
# Functions
|
||||
GET /functions # List all functions
|
||||
GET /functions/<address> # Function details
|
||||
GET /functions/<address>/decompile # Decompiled C code
|
||||
GET /functions/<address>/disassembly # Assembly listing
|
||||
GET /functions/<address>/variables # Local variables
|
||||
|
||||
# Analysis
|
||||
GET /analysis/callgraph?name=main&max_depth=3
|
||||
GET /analysis/dataflow?address=<addr>&direction=forward
|
||||
|
||||
# Memory
|
||||
GET /memory/<address>?length=256&format=hex
|
||||
POST /memory/<address> # Write bytes
|
||||
|
||||
# Data & Structures
|
||||
GET /data/strings
|
||||
GET /structs
|
||||
GET /xrefs?to_addr=<addr>
|
||||
```
|
||||
|
||||
### Response Format (HATEOAS)
|
||||
|
||||
All responses include navigation links:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"name": "main",
|
||||
"address": "00101380",
|
||||
"signature": "int main(void)"
|
||||
},
|
||||
"_links": {
|
||||
"self": "/functions/00101380",
|
||||
"decompile": "/functions/00101380/decompile",
|
||||
"disassembly": "/functions/00101380/disassembly"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Tools Overview
|
||||
|
||||
Use with Claude Code or any MCP client:
|
||||
|
||||
```python
|
||||
# Functions
|
||||
functions_list(port=8192, page_size=50)
|
||||
functions_decompile(address="00101380", port=8192)
|
||||
functions_get(name="main", port=8192)
|
||||
|
||||
# Analysis
|
||||
analysis_get_callgraph(name="main", max_depth=3, port=8192)
|
||||
analysis_get_dataflow(address="00101380", direction="forward", port=8192)
|
||||
|
||||
# Data
|
||||
data_list_strings(port=8192, grep="password")
|
||||
structs_list(port=8192)
|
||||
|
||||
# Docker Management
|
||||
docker_status()
|
||||
docker_start(binary_path="/path/to/binary", port=8192)
|
||||
docker_stop(name_or_id="container-name")
|
||||
docker_logs(name_or_id="container-name", tail=100)
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Find Interesting Functions
|
||||
|
||||
```bash
|
||||
# List all functions
|
||||
curl http://localhost:8192/functions | jq '.functions[].name'
|
||||
|
||||
# Search for crypto-related functions
|
||||
curl http://localhost:8192/functions | jq '.functions[] | select(.name | test("crypt|hash|encrypt"; "i"))'
|
||||
|
||||
# Get call graph from main
|
||||
curl 'http://localhost:8192/analysis/callgraph?name=main&max_depth=2' | jq .
|
||||
```
|
||||
|
||||
### Analyze Strings
|
||||
|
||||
```bash
|
||||
# List all strings
|
||||
curl http://localhost:8192/data/strings | jq '.strings[] | {address, value}'
|
||||
|
||||
# Find passwords/keys
|
||||
curl http://localhost:8192/data/strings | jq '.strings[] | select(.value | test("password|key|secret"; "i"))'
|
||||
```
|
||||
|
||||
### Decompile Entry Point
|
||||
|
||||
```bash
|
||||
# Get program entry point
|
||||
ENTRY=$(curl -s http://localhost:8192/program | jq -r '.program.entryPoint')
|
||||
|
||||
# Decompile it
|
||||
curl "http://localhost:8192/functions/$ENTRY/decompile" | jq -r '.result'
|
||||
```
|
||||
|
||||
## Docker Management
|
||||
|
||||
### List Running Containers
|
||||
|
||||
```bash
|
||||
docker ps | grep mcghidra
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
docker logs -f my-analysis
|
||||
```
|
||||
|
||||
### Stop Analysis
|
||||
|
||||
```bash
|
||||
docker stop my-analysis
|
||||
docker rm my-analysis
|
||||
```
|
||||
|
||||
### Persistent Projects
|
||||
|
||||
```bash
|
||||
# Mount project directory for persistence
|
||||
docker run -d --name persistent \
|
||||
-p 8192:8192 \
|
||||
-v $(pwd)/projects:/projects \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
-e PROJECT_NAME=MyProject \
|
||||
mcghidra:latest \
|
||||
/binaries/my-binary
|
||||
|
||||
# Projects are saved in ./projects/MyProject/
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Import Failed
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
docker logs my-analysis 2>&1 | grep ERROR
|
||||
|
||||
# Common issues:
|
||||
# 1. Binary not found → Check volume mount path
|
||||
# 2. AutoImporter failed → Use arm_firmware_prep.py for raw binaries
|
||||
# 3. Unsupported format → Check file type with `file binary`
|
||||
```
|
||||
|
||||
### Script Errors
|
||||
|
||||
```bash
|
||||
# If you see "Failed to get OSGi bundle" errors
|
||||
# Fix script permissions in running container:
|
||||
docker exec my-analysis sh -c 'chmod 644 /opt/ghidra/scripts/*.java'
|
||||
|
||||
# Then restart the analysis
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Use different port
|
||||
docker run -d --name analysis2 \
|
||||
-p 8193:8192 \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
mcghidra:latest \
|
||||
/binaries/binary
|
||||
|
||||
# Access at http://localhost:8193/
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Analyze Test Binary
|
||||
|
||||
```bash
|
||||
# Create simple test binary
|
||||
cat > test.c << 'EOF'
|
||||
#include <stdio.h>
|
||||
int secret_value = 0x42;
|
||||
void hidden() { printf("Hidden: %d\n", secret_value); }
|
||||
int main() { printf("Hello!\n"); return 0; }
|
||||
EOF
|
||||
|
||||
gcc -o binaries/test test.c
|
||||
|
||||
# Analyze
|
||||
docker run -d --name test-analysis \
|
||||
-p 8192:8192 \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
mcghidra:latest \
|
||||
/binaries/test
|
||||
|
||||
# Find hidden function
|
||||
sleep 15
|
||||
curl http://localhost:8192/functions | jq '.functions[] | select(.name == "hidden")'
|
||||
```
|
||||
|
||||
### Example 2: Cisco Phone Firmware
|
||||
|
||||
```bash
|
||||
# Prepare firmware
|
||||
python3 docker/arm_firmware_prep.py \
|
||||
cisco-firmware/P003-8-12-00.bin \
|
||||
binaries/cisco.elf \
|
||||
0x00000000
|
||||
|
||||
# Analyze
|
||||
docker run -d --name cisco \
|
||||
-p 8192:8192 \
|
||||
-v $(pwd)/binaries:/binaries \
|
||||
mcghidra:latest \
|
||||
/binaries/cisco.elf
|
||||
|
||||
# Explore
|
||||
sleep 30
|
||||
curl http://localhost:8192/functions | jq '.functions | length' # Function count
|
||||
curl http://localhost:8192/data/strings | jq '.strings[] | select(.value | test("SIP|RTP"))'
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Read ARM_FIRMWARE_SUCCESS.md** for ARM firmware details
|
||||
- **Check docker/README_ARM_SOLUTION.md** for advanced ARM workflows
|
||||
- **Explore MCP integration** with Claude Code
|
||||
- **Build automations** using the HTTP API
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
MCGhidra/
|
||||
├── docker/
|
||||
│ ├── Dockerfile # Main Docker image
|
||||
│ ├── entrypoint.sh # Container entry point
|
||||
│ ├── MCGhidraServer.java # HTTP API server (1724 lines)
|
||||
│ ├── ImportRawARM.java # Raw binary import script
|
||||
│ ├── arm_firmware_prep.py # ELF wrapper tool ⭐
|
||||
│ └── README*.md # Documentation
|
||||
├── src/mcghidra/ # MCP server implementation
|
||||
│ ├── __init__.py
|
||||
│ ├── server.py # FastMCP server
|
||||
│ └── mixins/ # Modular functionality
|
||||
│ ├── docker.py # Docker management
|
||||
│ ├── instances.py # Instance registry
|
||||
│ ├── functions.py # Function operations
|
||||
│ ├── analysis.py # Analysis tools
|
||||
│ └── ...
|
||||
├── binaries/ # Binary files for analysis
|
||||
├── projects/ # Ghidra project persistence
|
||||
└── launch.sh # MCP server launcher
|
||||
```
|
||||
|
||||
Happy reverse engineering! 🔍
|
||||
678
README.md
678
README.md
@ -1,462 +1,302 @@
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/releases)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/blob/main/GHIDRA_HTTP_API.md)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/stargazers)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/network/members)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/graphs/contributors)
|
||||
[](https://github.com/teal-bauer/GhydraMCP/actions/workflows/build.yml)
|
||||
# MCGhidra
|
||||
|
||||
# GhydraMCP v2.0
|
||||
**AI-native reverse engineering.** Give Claude (or any MCP client) direct access to Ghidra's analysis engine.
|
||||
|
||||
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).
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
GhydraMCP v2.0 integrates three key components:
|
||||
|
||||
1. **Modular Ghidra Plugin**: Exposes Ghidra's powerful reverse engineering capabilities through a HATEOAS-driven REST API
|
||||
2. **MCP Bridge**: A Python script that translates MCP requests into API calls with comprehensive type checking
|
||||
3. **Multi-instance Architecture**: Connect multiple Ghidra instances to analyze different binaries simultaneously
|
||||
|
||||
This architecture enables AI assistants like Claude to seamlessly:
|
||||
- Decompile and analyze binary code with customizable output formats
|
||||
- Map program structures, function relationships, and complex data types
|
||||
- Perform advanced binary analysis (cross-references, call graphs, data flow, etc.)
|
||||
- Make precise modifications to the analysis (rename, annotate, create/delete/modify data, etc.)
|
||||
- Read memory directly and manipulate binary at a low level
|
||||
- Navigate resources through discoverable HATEOAS links
|
||||
|
||||
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.
|
||||
|
||||
# Features
|
||||
|
||||
GhydraMCP version 2.0 provides a comprehensive set of reverse engineering capabilities to AI assistants through its HATEOAS-driven API:
|
||||
|
||||
## Advanced Program Analysis
|
||||
|
||||
- **Enhanced Decompilation**:
|
||||
- Convert binary functions to readable C code
|
||||
- Toggle between clean C-like pseudocode and raw decompiler output
|
||||
- Show/hide syntax trees for detailed analysis
|
||||
- Multiple simplification styles for different analysis approaches
|
||||
|
||||
- **Comprehensive Static Analysis**:
|
||||
- Cross-reference analysis (find callers and callees)
|
||||
- Complete call graph generation and traversal
|
||||
- Data flow analysis with variable tracking
|
||||
- Type propagation and reconstruction
|
||||
- Function relationship mapping
|
||||
|
||||
- **Memory Operations**:
|
||||
- Direct memory reading with hex and raw byte representation
|
||||
- Address space navigation and mapping
|
||||
- Memory segment analysis
|
||||
|
||||
- **Symbol Management**:
|
||||
- View and analyze imports and exports
|
||||
- Identify library functions and dependencies
|
||||
- Symbol table exploration and manipulation
|
||||
- Namespace hierarchy visualization
|
||||
|
||||
## Interactive Reverse Engineering
|
||||
|
||||
- **Code Understanding**:
|
||||
- Explore function code with rich context
|
||||
- Analyze data structures and complex types
|
||||
- View disassembly with linking to decompiled code
|
||||
- Examine function prototypes and signatures
|
||||
|
||||
- **Comprehensive Annotation**:
|
||||
- Rename functions, variables, and data
|
||||
- Add multiple comment types (EOL, plate, pre/post)
|
||||
- Create and modify data types
|
||||
- Set and update function signatures and prototypes
|
||||
|
||||
## Complete Data Manipulation
|
||||
|
||||
- **Data Creation and Management**:
|
||||
- Create new data items with specified types
|
||||
- Delete existing data items
|
||||
- Rename data items with proper scope handling
|
||||
- Set and update data types for existing items
|
||||
- Combined rename and retype operations
|
||||
- Type definition management
|
||||
|
||||
- **Function Manipulation**:
|
||||
- Rename functions with proper scoping
|
||||
- Update function signatures with parameter information
|
||||
- Modify local variable names and types
|
||||
- Set function return types
|
||||
|
||||
## Multi-instance Support
|
||||
|
||||
- Run multiple Ghidra instances simultaneously
|
||||
- Analyze different binaries in parallel
|
||||
- Connect to specific instances using port numbers
|
||||
- Auto-discovery of running Ghidra instances
|
||||
- Instance metadata with project and file information
|
||||
- Plugin version and API checking for compatibility
|
||||
|
||||
## Program Navigation and Discovery
|
||||
|
||||
- List and search functions, classes, and namespaces
|
||||
- View memory segments and layout
|
||||
- Search by name, pattern, or signature
|
||||
- Resource discovery through HATEOAS links
|
||||
- Pagination for handling large result sets
|
||||
- Filtering capabilities across all resources
|
||||
|
||||
# Installation
|
||||
|
||||
## Prerequisites
|
||||
- Install [Ghidra](https://ghidra-sre.org)
|
||||
- Python3
|
||||
- MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
|
||||
|
||||
## Ghidra
|
||||
First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/releases) from this repository. The "Complete" artifact contains the zipped Ghidra plugin and the Python MCP bridge. Unpack the outer archive, then, add the plugin to Ghidra:
|
||||
|
||||
1. Run Ghidra
|
||||
2. Select `File` -> `Install Extensions`
|
||||
3. Click the `+` button
|
||||
4. Select the `GhydraMCP-2.0.0-beta.1.zip` (or your chosen version) 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
|
||||
|
||||
Theoretically, any MCP client should work with GhydraMCP. Two examples are given below.
|
||||
|
||||
## API Reference (Updated for v2.0)
|
||||
|
||||
### Available Tools
|
||||
|
||||
**Program Analysis**:
|
||||
- `list_functions`: List all functions (params: offset, limit)
|
||||
- `list_classes`: List all classes/namespaces (params: offset, limit)
|
||||
- `decompile_function`: Get decompiled C code (params: name or address)
|
||||
- `get_function`: Get function details (params: name or address)
|
||||
- `get_callgraph`: Get function call graph (params: address)
|
||||
- `list_segments`: View memory segments (params: offset, limit)
|
||||
- `list_imports`: List imported symbols (params: offset, limit)
|
||||
- `list_exports`: List exported functions (params: offset, limit)
|
||||
- `list_namespaces`: Show namespaces (params: offset, limit)
|
||||
- `list_data_items`: View data labels (params: offset, limit)
|
||||
- `search_functions_by_name`: Find functions (params: query, offset, limit)
|
||||
|
||||
**Function Operations**:
|
||||
- `rename_function`: Rename a function (params: name, new_name)
|
||||
- `set_function_signature`: Update function prototype (params: address, signature)
|
||||
- `set_comment`: Add comments (params: address, comment, comment_type)
|
||||
- `remove_comment`: Remove comments (params: address, comment_type)
|
||||
|
||||
**Memory Operations**:
|
||||
- `read_memory`: Read bytes from memory (params: address, length)
|
||||
- `get_disassembly`: Get disassembled instructions (params: address, length)
|
||||
|
||||
**Data Manipulation**:
|
||||
- `create_data`: Create new data at address (params: address, data_type)
|
||||
- `delete_data`: Delete data at address (params: address)
|
||||
- `set_data_type`: Change data type at address (params: address, data_type)
|
||||
- `rename_data`: Rename data at address (params: address, name)
|
||||
- `update_data`: Update both name and type (params: address, name, data_type)
|
||||
|
||||
**Instance Management**:
|
||||
- `list_instances`: List active Ghidra instances (no params)
|
||||
- `register_instance`: Register new instance (params: port, url)
|
||||
- `unregister_instance`: Remove instance (params: port)
|
||||
- `discover_instances`: Auto-discover running instances (params: host [optional])
|
||||
|
||||
**Example Usage**:
|
||||
```python
|
||||
# Program analysis
|
||||
client.use_tool("ghydra", "decompile_function", {"name": "main"})
|
||||
client.use_tool("ghydra", "get_function", {"address": "0x00401000"})
|
||||
client.use_tool("ghydra", "get_callgraph", {"address": "0x00401000"})
|
||||
|
||||
# Memory and disassembly operations
|
||||
client.use_tool("ghydra", "read_memory", {"address": "0x00401000", "length": 16})
|
||||
client.use_tool("ghydra", "get_disassembly", {"address": "0x00401000", "length": 32})
|
||||
|
||||
# Function operations
|
||||
client.use_tool("ghydra", "set_function_signature", {"address": "0x00401000", "signature": "int main(int argc, char **argv)"})
|
||||
client.use_tool("ghydra", "set_comment", {"address": "0x00401100", "comment": "This instruction initializes the counter", "comment_type": "plate"})
|
||||
|
||||
# Data manipulation
|
||||
client.use_tool("ghydra", "create_data", {"address": "0x00401234", "data_type": "int"})
|
||||
client.use_tool("ghydra", "set_data_type", {"address": "0x00401238", "data_type": "char *"})
|
||||
client.use_tool("ghydra", "rename_data", {"address": "0x00401234", "name": "my_variable"})
|
||||
client.use_tool("ghydra", "update_data", {"address": "0x00401238", "name": "ptr_var", "data_type": "char *"})
|
||||
client.use_tool("ghydra", "delete_data", {"address": "0x0040123C"})
|
||||
|
||||
# Instance management
|
||||
client.use_tool("ghydra", "register_instance", {"port": 8192, "url": "http://localhost:8192/"})
|
||||
client.use_tool("ghydra", "register_instance", {"port": 8193})
|
||||
|
||||
# Auto-discover instances
|
||||
client.use_tool("ghydra", "discover_instances") # Default host
|
||||
client.use_tool("ghydra", "discover_instances", {"host": "192.168.1.10"}) # Custom host
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ "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... │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Client Setup
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
### Claude Desktop Configuration
|
||||
## What You Get
|
||||
|
||||
**64 MCP tools** across 12 categories:
|
||||
|
||||
| Category | Tools | What it does |
|
||||
|----------|-------|--------------|
|
||||
| **Functions** | 11 | Decompile, disassemble, rename, set signatures, list variables |
|
||||
| **Data** | 8 | Create/modify data items, list strings, set types |
|
||||
| **Structs** | 7 | Create structs, add/update fields, manage data types |
|
||||
| **Symbols** | 9 | Create labels, rename symbols, list imports/exports |
|
||||
| **Analysis** | 6 | Call graphs, data flow, cross-references, run analysis |
|
||||
| **Memory** | 2 | Read/write raw bytes |
|
||||
| **Variables** | 4 | List/rename function variables, set types |
|
||||
| **Bookmarks** | 3 | Create/list/delete analysis bookmarks |
|
||||
| **Enums/Typedefs** | 4 | Create enum and typedef data types |
|
||||
| **Namespaces** | 2 | List namespaces and classes |
|
||||
| **Segments** | 1 | List memory segments with permissions |
|
||||
| **Docker** | 7 | Auto-start containers, health checks, session management |
|
||||
|
||||
**13 analysis prompts** for common RE workflows:
|
||||
- `malware_triage` — Quick capability assessment
|
||||
- `identify_crypto` — Find crypto functions and constants
|
||||
- `find_authentication` — Locate auth, license checks, credentials
|
||||
- `analyze_protocol` — Reverse network/file protocols
|
||||
- `trace_data_flow` — Taint analysis through functions
|
||||
- And 8 more specialized prompts...
|
||||
|
||||
**11 MCP resources** for quick enumeration without tool calls.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Docker (Easiest)
|
||||
|
||||
No Ghidra installation needed. Analyze binaries in isolated containers.
|
||||
|
||||
```bash
|
||||
# Build the image (once)
|
||||
cd MCGhidra && docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
|
||||
# Add to your MCP config
|
||||
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
|
||||
```
|
||||
|
||||
Then in Claude:
|
||||
```
|
||||
Analyze /path/to/suspicious.exe
|
||||
```
|
||||
|
||||
Claude will auto-start a container, wait for analysis, and begin work.
|
||||
|
||||
### Option 2: Native Ghidra
|
||||
|
||||
1. **Install the Ghidra plugin:**
|
||||
- Download latest [release](https://github.com/starsong-consulting/MCGhidra/releases)
|
||||
- In Ghidra: `File → Install Extensions → +` → select the `.zip`
|
||||
- Restart Ghidra
|
||||
- Enable in `File → Configure → Developer → MCGhidraPlugin`
|
||||
|
||||
2. **Add MCP server:**
|
||||
```bash
|
||||
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
|
||||
```
|
||||
|
||||
3. **Open a binary in Ghidra**, then ask Claude to analyze it.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
|
||||
│ Claude │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
|
||||
│ (or other │ stdio │ (Python) │ REST API │ Plugin │
|
||||
│ MCP client) │ │ │ │ (Java) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
- **Ghidra Plugin**: Exposes Ghidra's analysis via HTTP REST API (HATEOAS)
|
||||
- **MCGhidra Server**: Translates MCP tool calls to API requests
|
||||
- **Multi-instance**: Analyze multiple binaries simultaneously on different ports
|
||||
- **Session isolation**: Docker containers get unique ports, preventing conflicts
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Set Current Instance (Then Forget About Ports)
|
||||
|
||||
```python
|
||||
instances_list() # Discover running Ghidra instances
|
||||
instances_use(port=8192) # Set as current
|
||||
functions_list() # No port needed!
|
||||
data_list_strings(grep="password") # Uses current instance
|
||||
```
|
||||
|
||||
### Docker Workflow
|
||||
|
||||
```python
|
||||
# Start container (returns immediately)
|
||||
result = docker_auto_start(binary_path="/path/to/malware.exe")
|
||||
# → {port: 8195, message: "Poll docker_health(port=8195)..."}
|
||||
|
||||
# Poll until ready
|
||||
while True:
|
||||
health = docker_health(port=8195)
|
||||
if health["healthy"]:
|
||||
break
|
||||
# Can check docker_logs() while waiting
|
||||
|
||||
# Register and use
|
||||
instances_use(port=8195)
|
||||
functions_list() # Ready to analyze
|
||||
```
|
||||
|
||||
### 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
|
||||
{
|
||||
"mcpServers": {
|
||||
"ghydra": {
|
||||
"mcghidra": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
|
||||
],
|
||||
"env": {
|
||||
"GHIDRA_HYDRA_HOST": "localhost" // Optional - defaults to localhost
|
||||
}
|
||||
"args": ["run", "--directory", "/path/to/MCGhidra", "mcghidra"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **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`.
|
||||
|
||||
### 5ire Configuration
|
||||
1. Tool Key: ghydra
|
||||
2. Name: GhydraMCP
|
||||
3. Command: `uv run /ABSOLUTE_PATH_TO/bridge_mcp_hydra.py`
|
||||
|
||||
## Example Session
|
||||
|
||||
Below is an example of how an AI assistant might interact with multiple Ghidra instances using GhydraMCP:
|
||||
|
||||
**User:**
|
||||
```
|
||||
Check which Ghidra instances are currently running.
|
||||
**Claude Code**:
|
||||
```bash
|
||||
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
|
||||
```
|
||||
|
||||
**Assistant:**
|
||||
---
|
||||
|
||||
## Tool Reference
|
||||
|
||||
### Instance Management
|
||||
```
|
||||
I'll check which Ghidra instances are currently running by discovering any active instances.
|
||||
|
||||
View result from discover_instances from ghydra (local)
|
||||
{
|
||||
"found": 2,
|
||||
"instances": [
|
||||
{
|
||||
"port": 8192,
|
||||
"url": "http://localhost:8192",
|
||||
"result": "Registered instance on port 8192 at http://localhost:8192"
|
||||
},
|
||||
{
|
||||
"port": 8193,
|
||||
"url": "http://localhost:8193",
|
||||
"result": "Registered instance on port 8193 at http://localhost:8193"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
View result from list_instances from ghydra (local)
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"port": 8192,
|
||||
"url": "http://localhost:8192",
|
||||
"project": "TestProject",
|
||||
"file": "application.exe"
|
||||
},
|
||||
{
|
||||
"port": 8193,
|
||||
"url": "http://localhost:8193",
|
||||
"project": "FirmwareAnalysis",
|
||||
"file": "firmware.bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
I found two active Ghidra instances:
|
||||
1. Port 8192: Analyzing "application.exe" in project "TestProject"
|
||||
2. Port 8193: Analyzing "firmware.bin" in project "FirmwareAnalysis"
|
||||
|
||||
I'm ready to analyze these binaries. What would you like me to examine?
|
||||
instances_list # Discover Ghidra instances (use this first!)
|
||||
instances_use # Set current working instance
|
||||
instances_current # Show current instance info
|
||||
```
|
||||
|
||||
**User:**
|
||||
### Function Analysis
|
||||
```
|
||||
List the functions through ghydra for both instances and identify any related functionality.
|
||||
functions_list # List functions (supports grep, pagination)
|
||||
functions_get # Get function details by name or address
|
||||
functions_decompile # Decompile to C pseudocode
|
||||
functions_disassemble # Get assembly instructions
|
||||
functions_rename # Rename a function
|
||||
functions_set_signature # Set function prototype
|
||||
functions_set_comment # Add decompiler comment
|
||||
functions_create # Create function at address
|
||||
functions_variables # List local variables and parameters
|
||||
```
|
||||
|
||||
**Assistant:**
|
||||
### Data Operations
|
||||
```
|
||||
I'll list the functions from both instances and analyze them.
|
||||
|
||||
View result from list_functions from ghydra (local){
|
||||
`port`: 8192
|
||||
}
|
||||
[Function list for application.exe...]
|
||||
|
||||
View result from list_functions from ghydra (local){
|
||||
`port`: 8193
|
||||
}
|
||||
[Function list for firmware.bin...]
|
||||
|
||||
I notice some interesting functions in both binaries. Let me decompile a few to understand their behavior.
|
||||
|
||||
View result from get_function from ghydra (local){
|
||||
`port`: 8192,
|
||||
`name`: "process_command"
|
||||
}
|
||||
[Decompiled code for process_command...]
|
||||
|
||||
View result from get_function from ghydra (local){
|
||||
`port`: 8193,
|
||||
`name`: "handle_message"
|
||||
}
|
||||
[Decompiled code for handle_message...]
|
||||
|
||||
Based on this analysis, I can see these binaries communicate using a simple protocol where...
|
||||
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
|
||||
```
|
||||
|
||||
# JSON Communication
|
||||
|
||||
GhydraMCP uses structured JSON for all communication between the Python bridge and Java plugin. This ensures consistent and reliable data exchange.
|
||||
|
||||
## API Architecture
|
||||
|
||||
GhydraMCP v2.0 implements a comprehensive HATEOAS-driven REST API that follows hypermedia design principles:
|
||||
|
||||
### Core API Design
|
||||
|
||||
- **HATEOAS Architecture**: Each response includes navigational links for resource discovery
|
||||
- **Versioned Endpoints**: All requests verified against API version for compatibility
|
||||
- **Structured Responses**: Standardized JSON format with consistent field naming
|
||||
- **Proper HTTP Methods**: GET for retrieval, POST for creation, PATCH for updates, DELETE for removal
|
||||
- **Appropriate Status Codes**: Uses standard HTTP status codes for clear error handling
|
||||
|
||||
### Response Format
|
||||
|
||||
All responses follow this HATEOAS-driven format:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-123",
|
||||
"instance": "http://localhost:8192",
|
||||
"success": true,
|
||||
"result": "...",
|
||||
"timestamp": 1712159482123,
|
||||
"_links": {
|
||||
"self": {"href": "/endpoint/current"},
|
||||
"related": [
|
||||
{"href": "/endpoint/related1", "name": "Related Resource 1"},
|
||||
{"href": "/endpoint/related2", "name": "Related Resource 2"}
|
||||
]
|
||||
}
|
||||
}
|
||||
### Cross-References & Analysis
|
||||
```
|
||||
xrefs_list # Find cross-references to/from address
|
||||
analysis_get_callgraph # Generate call graph
|
||||
analysis_get_dataflow # Trace data flow forward/backward
|
||||
analysis_run # Trigger Ghidra auto-analysis
|
||||
```
|
||||
|
||||
For list responses, pagination information is included:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-123",
|
||||
"instance": "http://localhost:8192",
|
||||
"success": true,
|
||||
"result": [ ... objects ... ],
|
||||
"size": 150,
|
||||
"offset": 0,
|
||||
"limit": 50,
|
||||
"_links": {
|
||||
"self": { "href": "/functions?offset=0&limit=50" },
|
||||
"next": { "href": "/functions?offset=50&limit=50" },
|
||||
"prev": { "href": "/functions?offset=0&limit=50" }
|
||||
}
|
||||
}
|
||||
### Structs & Types
|
||||
```
|
||||
structs_list # List struct definitions
|
||||
structs_get # Get struct with all fields
|
||||
structs_create # Create new struct
|
||||
structs_add_field # Add field to struct
|
||||
structs_update_field # Modify existing field
|
||||
structs_delete # Remove struct
|
||||
enums_list / enums_create
|
||||
typedefs_list / typedefs_create
|
||||
```
|
||||
|
||||
Error responses include detailed information:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-123",
|
||||
"instance": "http://localhost:8192",
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "RESOURCE_NOT_FOUND",
|
||||
"message": "Function 'main' not found in current program"
|
||||
},
|
||||
"status_code": 404,
|
||||
"timestamp": 1712159482123,
|
||||
"_links": {
|
||||
"self": {"href": "/functions/main"}
|
||||
}
|
||||
}
|
||||
### Docker Management
|
||||
```
|
||||
docker_auto_start # Start container for binary (auto port allocation)
|
||||
docker_health # Check if container API is responding
|
||||
docker_status # List all containers and images
|
||||
docker_start # Manual container start
|
||||
docker_stop # Stop container (session-scoped)
|
||||
docker_logs # Get container logs
|
||||
docker_cleanup # Remove orphaned containers
|
||||
```
|
||||
|
||||
This HATEOAS approach enables resource discovery and self-documenting APIs, making integration and exploration significantly easier.
|
||||
See `--help` or the [API docs](GHIDRA_HTTP_API.md) for full parameter details.
|
||||
|
||||
# Testing
|
||||
---
|
||||
|
||||
GhydraMCP includes comprehensive test suites for both the HTTP API and MCP bridge. See [TESTING.md](TESTING.md) for details on running the tests.
|
||||
## Building from Source
|
||||
|
||||
## HTTP API Tests
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://github.com/starsong-consulting/MCGhidra
|
||||
cd MCGhidra
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
# Build Ghidra plugin
|
||||
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
|
||||
If you only need the Ghidra plugin:
|
||||
## Architecture
|
||||
|
||||
```
|
||||
mvn clean package -P plugin-only
|
||||
```
|
||||
MCGhidra is designed for AI agents:
|
||||
|
||||
## Build Complete Package Only
|
||||
If you only need the combined package:
|
||||
- **Lazy registration**: `instances_use` doesn't block — validates on first real call
|
||||
- **Non-blocking I/O**: All Docker/HTTP operations run in thread executors
|
||||
- **Session isolation**: Each MCP session gets unique container ports
|
||||
- **Cursor pagination**: Handle 100K+ item responses without context overflow
|
||||
- **Server-side grep**: Filter results before they hit the wire
|
||||
|
||||
```
|
||||
mvn clean package -P complete-only
|
||||
```
|
||||
Based on [GhidraMCP by Laurie Wired](https://github.com/LaurieWired/GhidraMCP/), evolved into a comprehensive RE platform.
|
||||
|
||||
The Ghidra plugin includes these files required for Ghidra to recognize the extension:
|
||||
- lib/GhydraMCP.jar
|
||||
- extension.properties
|
||||
- Module.manifest
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
||||
14
TESTING.md
14
TESTING.md
@ -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
|
||||
|
||||
- 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`)
|
||||
|
||||
## Running All Tests
|
||||
@ -34,7 +34,7 @@ The `test_http_api.py` script tests the HTTP API exposed by the Java plugin. It
|
||||
|
||||
### Running the HTTP API Tests
|
||||
|
||||
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
|
||||
1. Make sure Ghidra is running with the MCGhidra plugin loaded
|
||||
2. Run the tests:
|
||||
|
||||
```bash
|
||||
@ -57,7 +57,7 @@ The `test_mcp_client.py` script tests the MCP bridge functionality using the MCP
|
||||
|
||||
### Running the MCP Bridge Tests
|
||||
|
||||
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
|
||||
1. Make sure Ghidra is running with the MCGhidra plugin loaded
|
||||
2. Run the tests:
|
||||
|
||||
```bash
|
||||
@ -89,7 +89,7 @@ The test script will:
|
||||
|
||||
### HTTP API Tests
|
||||
|
||||
- If tests are skipped with "Ghidra server not running or not accessible", make sure Ghidra is running and the 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).
|
||||
|
||||
### MCP Bridge Tests
|
||||
@ -103,7 +103,7 @@ The test script will:
|
||||
|
||||
To add a new test for an HTTP endpoint:
|
||||
|
||||
1. Add a new test method to the `GhydraMCPHttpApiTests` class
|
||||
1. Add a new test method to the `MCGhidraHttpApiTests` class
|
||||
2. Use the `requests` library to make HTTP requests to the endpoint
|
||||
3. Verify the response using assertions
|
||||
|
||||
|
||||
1
binaries/.gitkeep
Normal file
1
binaries/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Add binaries here for analysis
|
||||
7780
bridge_mcp_hydra.py
7780
bridge_mcp_hydra.py
File diff suppressed because it is too large
Load Diff
116
docker-compose.yml
Normal file
116
docker-compose.yml
Normal file
@ -0,0 +1,116 @@
|
||||
# MCGhidra Docker Compose Configuration
|
||||
# Provides both development and production modes for Ghidra + MCGhidra
|
||||
#
|
||||
# Usage:
|
||||
# Development: docker compose up mcghidra-dev
|
||||
# Production: docker compose up mcghidra
|
||||
#
|
||||
# Set MODE in .env file to switch between dev/prod behaviors
|
||||
|
||||
services:
|
||||
# =============================================================================
|
||||
# Production Service - Optimized for stability and security
|
||||
# =============================================================================
|
||||
mcghidra:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
args:
|
||||
GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
|
||||
GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
|
||||
image: mcghidra:${MCGHIDRAMCP_VERSION:-latest}
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-server
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MCGHIDRA_PORT:-8192}:8192"
|
||||
volumes:
|
||||
# Mount binaries to analyze (read-only in prod)
|
||||
- ${BINARIES_PATH:-./binaries}:/binaries:ro
|
||||
# Persist Ghidra projects between runs
|
||||
- mcghidra-projects:/projects
|
||||
environment:
|
||||
- MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
|
||||
- MCGHIDRA_PORT=8192
|
||||
- MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-2G}
|
||||
- PROJECT_NAME=${PROJECT_NAME:-MCGhidra}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8192/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: ${MCGHIDRA_MAXMEM:-2G}
|
||||
profiles:
|
||||
- prod
|
||||
- default
|
||||
|
||||
# =============================================================================
|
||||
# Development Service - Hot-reload and debugging friendly
|
||||
# =============================================================================
|
||||
mcghidra-dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
args:
|
||||
GHIDRA_VERSION: ${GHIDRA_VERSION:-11.4.2}
|
||||
GHIDRA_DATE: ${GHIDRA_DATE:-20250826}
|
||||
image: mcghidra:dev
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-dev
|
||||
ports:
|
||||
- "${MCGHIDRA_PORT:-8192}:8192"
|
||||
# Additional ports for debugging/multiple instances
|
||||
- "8193:8193"
|
||||
- "8194:8194"
|
||||
volumes:
|
||||
# Mount binaries (read-write in dev)
|
||||
- ${BINARIES_PATH:-./binaries}:/binaries:rw
|
||||
# Persist projects
|
||||
- mcghidra-projects-dev:/projects
|
||||
# Mount scripts for live editing (development only)
|
||||
- ./docker/MCGhidraServer.java:/opt/ghidra/scripts/MCGhidraServer.java:ro
|
||||
- ./docker/entrypoint.sh:/entrypoint.sh:ro
|
||||
environment:
|
||||
- MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
|
||||
- MCGHIDRA_PORT=8192
|
||||
- MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-4G}
|
||||
- PROJECT_NAME=${PROJECT_NAME:-MCGhidra-Dev}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8192/"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 120s
|
||||
retries: 5
|
||||
profiles:
|
||||
- dev
|
||||
|
||||
# =============================================================================
|
||||
# Shell Service - Interactive debugging container
|
||||
# =============================================================================
|
||||
mcghidra-shell:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
image: mcghidra:${MCGHIDRAMCP_VERSION:-latest}
|
||||
container_name: ${COMPOSE_PROJECT_NAME:-mcghidra}-shell
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- ${BINARIES_PATH:-./binaries}:/binaries:rw
|
||||
- mcghidra-projects-dev:/projects
|
||||
environment:
|
||||
- MCGHIDRA_MODE=shell
|
||||
profiles:
|
||||
- debug
|
||||
|
||||
volumes:
|
||||
mcghidra-projects:
|
||||
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-projects
|
||||
mcghidra-projects-dev:
|
||||
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-projects-dev
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${COMPOSE_PROJECT_NAME:-mcghidra}-network
|
||||
152
docker/Dockerfile
Normal file
152
docker/Dockerfile
Normal file
@ -0,0 +1,152 @@
|
||||
# MCGhidra Docker Image
|
||||
# Ghidra + MCGhidra Plugin pre-installed for headless binary analysis
|
||||
#
|
||||
# Build: docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
# Run: docker run -p 8192:8192 -v /path/to/binaries:/binaries mcghidra:latest
|
||||
|
||||
ARG GHIDRA_VERSION=11.4.2
|
||||
ARG GHIDRA_DATE=20250826
|
||||
|
||||
# =============================================================================
|
||||
# Stage 1: Build the MCGhidra plugin
|
||||
# =============================================================================
|
||||
FROM eclipse-temurin:21-jdk-jammy AS builder
|
||||
|
||||
ARG GHIDRA_VERSION
|
||||
ARG GHIDRA_DATE
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
maven \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download and extract Ghidra
|
||||
WORKDIR /opt
|
||||
# Download with retries and resume support for unreliable connections
|
||||
RUN for i in 1 2 3 4 5; do \
|
||||
curl -fSL --http1.1 -C - \
|
||||
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
|
||||
-o ghidra.zip && break || sleep 30; \
|
||||
done \
|
||||
&& unzip -q ghidra.zip \
|
||||
&& rm ghidra.zip \
|
||||
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra
|
||||
|
||||
ENV GHIDRA_HOME=/opt/ghidra
|
||||
|
||||
# Copy MCGhidra source and build
|
||||
WORKDIR /build
|
||||
|
||||
# Copy pom.xml first and download dependencies (cached until pom.xml changes)
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:resolve -P plugin-only -q \
|
||||
-Dghidra.generic.jar=${GHIDRA_HOME}/Ghidra/Framework/Generic/lib/Generic.jar \
|
||||
-Dghidra.softwaremodeling.jar=${GHIDRA_HOME}/Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar \
|
||||
-Dghidra.project.jar=${GHIDRA_HOME}/Ghidra/Framework/Project/lib/Project.jar \
|
||||
-Dghidra.docking.jar=${GHIDRA_HOME}/Ghidra/Framework/Docking/lib/Docking.jar \
|
||||
-Dghidra.decompiler.jar=${GHIDRA_HOME}/Ghidra/Features/Decompiler/lib/Decompiler.jar \
|
||||
-Dghidra.utility.jar=${GHIDRA_HOME}/Ghidra/Framework/Utility/lib/Utility.jar \
|
||||
-Dghidra.base.jar=${GHIDRA_HOME}/Ghidra/Features/Base/lib/Base.jar \
|
||||
|| true
|
||||
|
||||
# Now copy source - only this layer rebuilds on code changes
|
||||
COPY src ./src
|
||||
|
||||
# Build the plugin (skip git-commit-id plugin since .git isn't in Docker context)
|
||||
RUN mvn package -P plugin-only -DskipTests \
|
||||
-Dmaven.gitcommitid.skip=true \
|
||||
-Dghidra.generic.jar=${GHIDRA_HOME}/Ghidra/Framework/Generic/lib/Generic.jar \
|
||||
-Dghidra.softwaremodeling.jar=${GHIDRA_HOME}/Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar \
|
||||
-Dghidra.project.jar=${GHIDRA_HOME}/Ghidra/Framework/Project/lib/Project.jar \
|
||||
-Dghidra.docking.jar=${GHIDRA_HOME}/Ghidra/Framework/Docking/lib/Docking.jar \
|
||||
-Dghidra.decompiler.jar=${GHIDRA_HOME}/Ghidra/Features/Decompiler/lib/Decompiler.jar \
|
||||
-Dghidra.utility.jar=${GHIDRA_HOME}/Ghidra/Framework/Utility/lib/Utility.jar \
|
||||
-Dghidra.base.jar=${GHIDRA_HOME}/Ghidra/Features/Base/lib/Base.jar
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime image with Ghidra + MCGhidra
|
||||
# =============================================================================
|
||||
# NOTE: Ghidra requires JDK (not JRE) - it checks for javac in LaunchSupport
|
||||
FROM eclipse-temurin:21-jdk-jammy AS runtime
|
||||
|
||||
ARG GHIDRA_VERSION
|
||||
ARG GHIDRA_DATE
|
||||
|
||||
LABEL org.opencontainers.image.title="mcghidra" \
|
||||
org.opencontainers.image.description="Ghidra + MCGhidra Plugin for AI-assisted reverse engineering" \
|
||||
org.opencontainers.image.source="https://github.com/starsong-consulting/MCGhidra" \
|
||||
org.opencontainers.image.licenses="Apache-2.0"
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
fontconfig \
|
||||
libfreetype6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -g 1001 ghidra && useradd -u 1001 -g ghidra -m -s /bin/bash ghidra
|
||||
|
||||
# Download and extract Ghidra (in runtime stage for cleaner image)
|
||||
WORKDIR /opt
|
||||
# Download with retries and resume support for unreliable connections
|
||||
RUN for i in 1 2 3 4 5; do \
|
||||
curl -fSL --http1.1 -C - \
|
||||
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
|
||||
-o ghidra.zip && break || sleep 30; \
|
||||
done \
|
||||
&& unzip -q ghidra.zip \
|
||||
&& rm ghidra.zip \
|
||||
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \
|
||||
&& chown -R ghidra:ghidra /opt/ghidra
|
||||
|
||||
ENV GHIDRA_HOME=/opt/ghidra
|
||||
ENV PATH="${GHIDRA_HOME}:${PATH}"
|
||||
|
||||
# Install the MCGhidra plugin
|
||||
COPY --from=builder /build/target/MCGhidra-*.zip /tmp/
|
||||
RUN mkdir -p /opt/ghidra/Ghidra/Extensions \
|
||||
&& unzip -q /tmp/MCGhidra-*.zip -d /opt/ghidra/Ghidra/Extensions/ \
|
||||
&& rm /tmp/MCGhidra-*.zip \
|
||||
&& chown -R ghidra:ghidra /opt/ghidra/Ghidra/Extensions/
|
||||
|
||||
# Create directories for projects and binaries
|
||||
RUN mkdir -p /projects /binaries /home/ghidra/.ghidra \
|
||||
&& chown -R ghidra:ghidra /projects /binaries /home/ghidra
|
||||
|
||||
# Copy MCGhidra Python scripts to user scripts directory
|
||||
# Python/Jython scripts don't require OSGi bundle registration - they work without issue
|
||||
RUN mkdir -p /home/ghidra/ghidra_scripts
|
||||
COPY docker/MCGhidraServer.py /home/ghidra/ghidra_scripts/
|
||||
COPY docker/ImportRawARM.java /home/ghidra/ghidra_scripts/
|
||||
|
||||
# Set proper ownership and permissions
|
||||
RUN chown -R ghidra:ghidra /home/ghidra/ghidra_scripts \
|
||||
&& chmod 755 /home/ghidra/ghidra_scripts/*.py 2>/dev/null || true \
|
||||
&& chmod 755 /home/ghidra/ghidra_scripts/*.java 2>/dev/null || true
|
||||
|
||||
# Copy entrypoint script (755 so ghidra user can read and execute)
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 755 /entrypoint.sh
|
||||
|
||||
# Switch to non-root user
|
||||
USER ghidra
|
||||
WORKDIR /home/ghidra
|
||||
|
||||
# Expose the MCGhidra HTTP API port (and additional ports for multiple instances)
|
||||
EXPOSE 8192 8193 8194 8195
|
||||
|
||||
# Default environment
|
||||
ENV MCGHIDRA_MODE=headless
|
||||
ENV MCGHIDRA_PORT=8192
|
||||
ENV MCGHIDRA_MAXMEM=2G
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:${MCGHIDRA_PORT}/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
204
docker/ImportRawARM.java
Normal file
204
docker/ImportRawARM.java
Normal file
@ -0,0 +1,204 @@
|
||||
// Import and analyze raw ARM firmware binary
|
||||
// This script imports a raw binary file with specified ARM processor and load address
|
||||
// @author MCGhidra
|
||||
// @category Binary.Import
|
||||
// @keybinding
|
||||
// @menupath
|
||||
// @toolbar
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import ghidra.app.util.opinion.BinaryLoader;
|
||||
import ghidra.app.util.opinion.LoadSpec;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainFolder;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.lang.LanguageCompilerSpecPair;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
public class ImportRawARM extends GhidraScript {
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] args = getScriptArgs();
|
||||
|
||||
if (args.length < 2) {
|
||||
println("========================================");
|
||||
println(" Import Raw ARM Firmware Binary");
|
||||
println("========================================");
|
||||
println("");
|
||||
println("Usage: -postScript ImportRawARM.java <binary_path> <load_address> [<language_id>]");
|
||||
println("");
|
||||
println("Arguments:");
|
||||
println(" binary_path - Path to raw binary file");
|
||||
println(" load_address - Base address in hex (e.g., 0x00000000)");
|
||||
println(" language_id - ARM language ID (default: ARM:LE:32:v5t)");
|
||||
println("");
|
||||
println("Common ARM Language IDs:");
|
||||
println(" ARM:LE:32:v4 - ARMv4 little-endian");
|
||||
println(" ARM:LE:32:v5 - ARMv5 little-endian");
|
||||
println(" ARM:LE:32:v5t - ARMv5T little-endian (Thumb)");
|
||||
println(" ARM:LE:32:v6 - ARMv6 little-endian");
|
||||
println(" ARM:LE:32:v7 - ARMv7 little-endian");
|
||||
println(" ARM:LE:32:Cortex - ARM Cortex");
|
||||
println(" ARM:BE:32:v5t - ARMv5T big-endian");
|
||||
println("");
|
||||
println("Example:");
|
||||
println(" analyzeHeadless /projects MyProject \\");
|
||||
println(" -postScript ImportRawARM.java /binaries/firmware.bin 0x00000000 ARM:LE:32:v5t");
|
||||
println("");
|
||||
return;
|
||||
}
|
||||
|
||||
String binaryPath = args[0];
|
||||
String loadAddressStr = args[1];
|
||||
String languageIDStr = args.length > 2 ? args[2] : "ARM:LE:32:v5t";
|
||||
|
||||
// Parse load address
|
||||
long loadAddress;
|
||||
if (loadAddressStr.toLowerCase().startsWith("0x")) {
|
||||
loadAddress = Long.parseLong(loadAddressStr.substring(2), 16);
|
||||
} else {
|
||||
loadAddress = Long.parseLong(loadAddressStr, 16);
|
||||
}
|
||||
|
||||
File binaryFile = new File(binaryPath);
|
||||
if (!binaryFile.exists()) {
|
||||
printerr("ERROR: Binary file not found: " + binaryPath);
|
||||
return;
|
||||
}
|
||||
|
||||
long fileSize = binaryFile.length();
|
||||
|
||||
println("========================================");
|
||||
println(" Importing Raw ARM Firmware");
|
||||
println("========================================");
|
||||
println(" Binary: " + binaryFile.getName());
|
||||
println(" Size: " + fileSize + " bytes (0x" + Long.toHexString(fileSize) + ")");
|
||||
println(" Load Address: 0x" + String.format("%08X", loadAddress));
|
||||
println(" Language: " + languageIDStr);
|
||||
println("========================================");
|
||||
println("");
|
||||
|
||||
try {
|
||||
// Read the binary file
|
||||
byte[] bytes = Files.readAllBytes(binaryFile.toPath());
|
||||
ByteArrayProvider provider = new ByteArrayProvider(bytes);
|
||||
|
||||
// Parse language string (format: "ARM:LE:32:v5t")
|
||||
String[] parts = languageIDStr.split(":");
|
||||
if (parts.length < 4) {
|
||||
printerr("ERROR: Invalid language ID format. Expected format: ARCH:ENDIAN:SIZE:VARIANT");
|
||||
printerr(" Example: ARM:LE:32:v5t");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create language compiler spec pair
|
||||
LanguageCompilerSpecPair lcsPair = new LanguageCompilerSpecPair(languageIDStr, "default");
|
||||
|
||||
// Use BinaryLoader to import
|
||||
BinaryLoader loader = new BinaryLoader();
|
||||
|
||||
// Create load spec with our language
|
||||
LoadSpec loadSpec = new LoadSpec(loader, loadAddress, lcsPair, false);
|
||||
|
||||
// Get project folder
|
||||
DomainFolder rootFolder = state.getProject().getProjectData().getRootFolder();
|
||||
|
||||
String programName = binaryFile.getName();
|
||||
|
||||
println("Creating program: " + programName);
|
||||
println("Using language: " + languageIDStr);
|
||||
println("");
|
||||
|
||||
// Import using BinaryLoader
|
||||
MessageLog log = new MessageLog();
|
||||
List<Program> programs = loader.load(
|
||||
provider,
|
||||
programName,
|
||||
rootFolder,
|
||||
loadSpec,
|
||||
List.of(), // options
|
||||
log,
|
||||
this, // consumer
|
||||
monitor
|
||||
);
|
||||
|
||||
if (programs == null || programs.isEmpty()) {
|
||||
printerr("ERROR: Failed to load binary");
|
||||
println("");
|
||||
println("Loader messages:");
|
||||
println(log.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
Program program = programs.get(0);
|
||||
|
||||
println("Import successful!");
|
||||
println(" Program: " + program.getName());
|
||||
println(" Base Address: " + program.getImageBase());
|
||||
println(" Memory blocks: " + program.getMemory().getBlocks().length);
|
||||
println("");
|
||||
|
||||
// Set entry point at load address
|
||||
Address entryAddr = program.getAddressFactory().getDefaultAddressSpace().getAddress(loadAddress);
|
||||
|
||||
int txId = program.startTransaction("Set Entry Point");
|
||||
try {
|
||||
program.getSymbolTable().addExternalEntryPoint(entryAddr);
|
||||
program.getSymbolTable().createLabel(entryAddr, "entry",
|
||||
ghidra.program.model.symbol.SourceType.ANALYSIS);
|
||||
program.endTransaction(txId, true);
|
||||
|
||||
println("Entry point set at: " + entryAddr);
|
||||
println("");
|
||||
} catch (Exception e) {
|
||||
program.endTransaction(txId, false);
|
||||
printerr("Warning: Could not set entry point: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Save the program
|
||||
try {
|
||||
program.save("ARM firmware import", monitor);
|
||||
println("Program saved successfully");
|
||||
println("");
|
||||
} catch (Exception e) {
|
||||
printerr("Warning: Could not save program: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
println("Starting auto-analysis...");
|
||||
println("(This may take a while for large binaries)");
|
||||
println("");
|
||||
|
||||
analyzeAll(program);
|
||||
|
||||
println("");
|
||||
println("========================================");
|
||||
println(" Import Complete!");
|
||||
println("========================================");
|
||||
println(" Program: " + program.getName());
|
||||
println(" Functions found: " + program.getFunctionManager().getFunctionCount());
|
||||
println(" Defined data: " + program.getListing().getNumDefinedData());
|
||||
println("========================================");
|
||||
println("");
|
||||
|
||||
// Set as current program for subsequent scripts
|
||||
state.setCurrentProgram(program);
|
||||
|
||||
} catch (IOException e) {
|
||||
printerr("ERROR: Failed to read binary file: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
printerr("ERROR: Import failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
2843
docker/MCGhidraServer.py
Normal file
2843
docker/MCGhidraServer.py
Normal file
File diff suppressed because it is too large
Load Diff
197
docker/README.md
Normal file
197
docker/README.md
Normal file
@ -0,0 +1,197 @@
|
||||
# MCGhidra Docker Setup
|
||||
|
||||
This directory contains Docker configuration for running MCGhidra in headless mode.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
|
||||
# Analyze a binary
|
||||
docker run -p 8192:8192 -v /path/to/binaries:/binaries mcghidra /binaries/sample.exe
|
||||
|
||||
# Check API health
|
||||
curl http://localhost:8192/
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Docker container includes:
|
||||
|
||||
1. **Ghidra 11.4.2** - Full headless installation
|
||||
2. **MCGhidra Extension** - The Java plugin (installed in Extensions/)
|
||||
3. **MCGhidraServer.py** - Headless HTTP server (Jython, full API parity)
|
||||
|
||||
### Why Two HTTP Servers?
|
||||
|
||||
The MCGhidra plugin (`MCGhidraPlugin.java`) is a full Ghidra GUI plugin that requires:
|
||||
- Ghidra's `PluginTool` framework
|
||||
- `ProgramManager` service for program access
|
||||
- GUI event handling
|
||||
|
||||
These GUI services don't exist in headless mode. Instead, the container uses `MCGhidraServer.py`, a Jython script that:
|
||||
- Runs via `analyzeHeadless -postScript`
|
||||
- Has direct access to `currentProgram` from the script context
|
||||
- Provides **full API parity** with the GUI plugin (45 routes)
|
||||
- Supports all read and write operations
|
||||
|
||||
### Available Endpoints (Headless Mode)
|
||||
|
||||
The headless server implements the complete MCGhidra HTTP API:
|
||||
|
||||
| Category | Endpoints | Description |
|
||||
|----------|-----------|-------------|
|
||||
| **Info** | `GET /`, `/info`, `/program` | API info, program metadata |
|
||||
| **Functions** | `GET /functions`, `/functions/{addr}`, `/functions/by-name/{name}` | List and detail |
|
||||
| **Decompile** | `GET /functions/{addr}/decompile`, `/functions/by-name/{name}/decompile` | C pseudocode |
|
||||
| **Disassembly** | `GET /functions/{addr}/disassembly`, `/functions/by-name/{name}/disassembly` | Assembly listing |
|
||||
| **Data** | `GET /data`, `/strings` | Defined data and strings |
|
||||
| **Memory** | `GET /memory`, `/memory/blocks` | Read bytes, list segments |
|
||||
| **Xrefs** | `GET /xrefs` | Cross-references (to/from) |
|
||||
| **Structs** | `GET /structs` | Data type structures |
|
||||
| **Symbols** | `GET /symbols`, `/imports`, `/exports` | Symbol tables |
|
||||
| **Analysis** | `GET /analysis/callgraph`, `/analysis/dataflow` | Static analysis |
|
||||
| **Write Ops** | `PATCH /functions/*`, `POST /data`, `POST /structs/*` | Rename, annotate, create |
|
||||
|
||||
See [GHIDRA_HTTP_API.md](../GHIDRA_HTTP_API.md) for the complete API specification.
|
||||
|
||||
## Container Modes
|
||||
|
||||
### Headless Mode (Default)
|
||||
|
||||
Imports a binary, analyzes it, and starts the HTTP API server:
|
||||
|
||||
```bash
|
||||
docker run -p 8192:8192 \
|
||||
-v ./samples:/binaries \
|
||||
mcghidra /binaries/sample.exe
|
||||
```
|
||||
|
||||
### Server Mode
|
||||
|
||||
Opens an existing project and program:
|
||||
|
||||
```bash
|
||||
docker run -p 8192:8192 \
|
||||
-e MCGHIDRA_MODE=server \
|
||||
-v ./projects:/projects \
|
||||
mcghidra program_name
|
||||
```
|
||||
|
||||
### Analyze Mode
|
||||
|
||||
Imports and analyzes without starting HTTP server:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e MCGHIDRA_MODE=analyze \
|
||||
-v ./samples:/binaries \
|
||||
-v ./projects:/projects \
|
||||
mcghidra /binaries/sample.exe
|
||||
```
|
||||
|
||||
### Shell Mode
|
||||
|
||||
Interactive debugging:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
-e MCGHIDRA_MODE=shell \
|
||||
mcghidra
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRA_MODE` | `headless` | Container mode (headless, server, analyze, shell) |
|
||||
| `MCGHIDRA_PORT` | `8192` | HTTP API port |
|
||||
| `MCGHIDRA_MAXMEM` | `2G` | JVM heap memory |
|
||||
| `PROJECT_DIR` | `/projects` | Ghidra project directory |
|
||||
| `PROJECT_NAME` | `MCGhidra` | Ghidra project name |
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Use docker-compose for easier management:
|
||||
|
||||
```bash
|
||||
# Development mode (hot-reload scripts)
|
||||
docker compose --profile dev up mcghidra-dev
|
||||
|
||||
# Production mode
|
||||
docker compose --profile prod up mcghidra
|
||||
|
||||
# Interactive shell
|
||||
docker compose --profile debug run --rm mcghidra-shell
|
||||
```
|
||||
|
||||
## MCP Integration
|
||||
|
||||
The MCGhidra Python server includes Docker management tools:
|
||||
|
||||
```python
|
||||
# Check Docker status
|
||||
await docker_status()
|
||||
|
||||
# Start container for a binary
|
||||
await docker_start(binary_path="/path/to/binary.exe", port=8192)
|
||||
|
||||
# Wait for container to be ready
|
||||
await docker_wait(port=8192, timeout=300)
|
||||
|
||||
# Automatic mode - starts container if no Ghidra available
|
||||
await docker_auto_start(binary_path="/path/to/binary.exe")
|
||||
|
||||
# Get container logs
|
||||
await docker_logs("mcghidra-server")
|
||||
|
||||
# Stop container
|
||||
await docker_stop("mcghidra-server")
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Using Make
|
||||
make build
|
||||
|
||||
# Using Docker directly
|
||||
docker build -t mcghidra:latest -f docker/Dockerfile .
|
||||
|
||||
# Build with specific Ghidra version
|
||||
docker build -t mcghidra:latest \
|
||||
--build-arg GHIDRA_VERSION=11.4.2 \
|
||||
--build-arg GHIDRA_DATE=20250826 \
|
||||
-f docker/Dockerfile .
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container starts but API doesn't respond
|
||||
|
||||
Analysis takes time. Monitor progress with:
|
||||
```bash
|
||||
docker logs -f mcghidra-server
|
||||
```
|
||||
|
||||
### Port already in use
|
||||
|
||||
Stop existing containers:
|
||||
```bash
|
||||
docker stop $(docker ps -q --filter "name=mcghidra")
|
||||
```
|
||||
|
||||
### Memory issues with large binaries
|
||||
|
||||
Increase JVM heap:
|
||||
```bash
|
||||
docker run -e MCGHIDRA_MAXMEM=4G -p 8192:8192 mcghidra /binaries/large.exe
|
||||
```
|
||||
|
||||
### Permission denied on volumes
|
||||
|
||||
The container runs as user `ghidra` (UID 1001). Ensure volume permissions:
|
||||
```bash
|
||||
sudo chown -R 1001:1001 /path/to/binaries
|
||||
```
|
||||
178
docker/entrypoint.sh
Executable file
178
docker/entrypoint.sh
Executable file
@ -0,0 +1,178 @@
|
||||
#!/bin/bash
|
||||
# MCGhidra Docker Entrypoint
|
||||
# Starts Ghidra in headless mode with HTTP API server
|
||||
|
||||
set -e
|
||||
|
||||
MCGHIDRA_MODE=${MCGHIDRA_MODE:-headless}
|
||||
MCGHIDRA_PORT=${MCGHIDRA_PORT:-8192}
|
||||
MCGHIDRA_MAXMEM=${MCGHIDRA_MAXMEM:-2G}
|
||||
GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra}
|
||||
# User scripts directory - Python scripts don't need OSGi bundle registration
|
||||
SCRIPT_DIR=${SCRIPT_DIR:-/home/ghidra/ghidra_scripts}
|
||||
|
||||
# Project settings
|
||||
PROJECT_DIR=${PROJECT_DIR:-/projects}
|
||||
PROJECT_NAME=${PROJECT_NAME:-MCGhidra}
|
||||
|
||||
echo "=============================================="
|
||||
echo " MCGhidra Docker Container"
|
||||
echo "=============================================="
|
||||
echo " Mode: ${MCGHIDRA_MODE}"
|
||||
echo " Port: ${MCGHIDRA_PORT}"
|
||||
echo " Memory: ${MCGHIDRA_MAXMEM}"
|
||||
echo " Project: ${PROJECT_DIR}/${PROJECT_NAME}"
|
||||
echo "=============================================="
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "${PROJECT_DIR}"
|
||||
|
||||
# Handle different modes
|
||||
case "${MCGHIDRA_MODE}" in
|
||||
headless)
|
||||
# Headless mode: Import a binary and start HTTP server
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo ""
|
||||
echo "Usage: docker run mcghidra:latest [binary_path] [options]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Analyze a binary mounted at /binaries/sample.exe"
|
||||
echo " docker run -p 8192:8192 -v ./samples:/binaries mcghidra /binaries/sample.exe"
|
||||
echo ""
|
||||
echo " # With custom project name"
|
||||
echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware mcghidra /binaries/sample.exe"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " MCGHIDRA_PORT - HTTP API port (default: 8192)"
|
||||
echo " MCGHIDRA_MAXMEM - Max JVM heap (default: 2G)"
|
||||
echo " PROJECT_NAME - Ghidra project name (default: MCGhidra)"
|
||||
echo " PROJECT_DIR - Project directory (default: /projects)"
|
||||
echo " GHIDRA_LANGUAGE - Processor language ID (e.g., ARM:LE:32:v4t)"
|
||||
echo " GHIDRA_BASE_ADDRESS - Base address for raw binaries (e.g., 0x00000000)"
|
||||
echo " GHIDRA_LOADER - Loader type (e.g., BinaryLoader for raw firmware)"
|
||||
echo ""
|
||||
echo "Starting in wait mode..."
|
||||
echo "Container will stay running for debugging or manual operation."
|
||||
echo "You can exec into this container to run analyzeHeadless manually."
|
||||
echo ""
|
||||
|
||||
# Keep container alive for debugging/manual operation
|
||||
tail -f /dev/null
|
||||
else
|
||||
BINARY_PATH="$1"
|
||||
shift
|
||||
|
||||
if [ ! -f "${BINARY_PATH}" ]; then
|
||||
echo "ERROR: Binary not found: ${BINARY_PATH}"
|
||||
echo "Make sure to mount the binary directory with -v /host/path:/binaries"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_NAME=$(basename "${BINARY_PATH}")
|
||||
echo "Importing and analyzing: ${BINARY_NAME}"
|
||||
echo ""
|
||||
|
||||
# Build the analyzeHeadless command
|
||||
ANALYZE_CMD="${GHIDRA_HOME}/support/analyzeHeadless"
|
||||
ANALYZE_ARGS=(
|
||||
"${PROJECT_DIR}"
|
||||
"${PROJECT_NAME}"
|
||||
-import "${BINARY_PATH}"
|
||||
-max-cpu 2
|
||||
-scriptPath "${SCRIPT_DIR}"
|
||||
-postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}"
|
||||
)
|
||||
|
||||
# Optional: processor/language for raw binaries
|
||||
if [ -n "${GHIDRA_LANGUAGE}" ]; then
|
||||
if ! echo "${GHIDRA_LANGUAGE}" | grep -qE '^[A-Za-z0-9_]+:[A-Z]{2}:[0-9]+:[A-Za-z0-9._-]+$'; then
|
||||
echo "ERROR: Invalid GHIDRA_LANGUAGE format: ${GHIDRA_LANGUAGE}"
|
||||
echo "Expected: ARCH:ENDIAN:SIZE:VARIANT (e.g., ARM:LE:32:v4t)"
|
||||
exit 1
|
||||
fi
|
||||
ANALYZE_ARGS+=(-processor "${GHIDRA_LANGUAGE}")
|
||||
fi
|
||||
|
||||
# Optional: base address
|
||||
if [ -n "${GHIDRA_BASE_ADDRESS}" ]; then
|
||||
if ! echo "${GHIDRA_BASE_ADDRESS}" | grep -qE '^(0x)?[0-9a-fA-F]+$'; then
|
||||
echo "ERROR: Invalid GHIDRA_BASE_ADDRESS format: ${GHIDRA_BASE_ADDRESS}"
|
||||
echo "Expected hex: 0x00000000 or 00000000"
|
||||
exit 1
|
||||
fi
|
||||
ANALYZE_ARGS+=(-loader-baseAddr "${GHIDRA_BASE_ADDRESS}")
|
||||
fi
|
||||
|
||||
# Optional: explicit loader (e.g., BinaryLoader for raw firmware)
|
||||
if [ -n "${GHIDRA_LOADER}" ]; then
|
||||
if ! echo "${GHIDRA_LOADER}" | grep -qE '^[A-Za-z0-9_]+$'; then
|
||||
echo "ERROR: Invalid GHIDRA_LOADER format: ${GHIDRA_LOADER}"
|
||||
echo "Expected alphanumeric name (e.g., BinaryLoader)"
|
||||
exit 1
|
||||
fi
|
||||
ANALYZE_ARGS+=(-loader "${GHIDRA_LOADER}")
|
||||
fi
|
||||
|
||||
# Add any extra arguments passed
|
||||
ANALYZE_ARGS+=("$@")
|
||||
|
||||
echo "Running: ${ANALYZE_CMD} ${ANALYZE_ARGS[*]}"
|
||||
echo ""
|
||||
|
||||
exec "${ANALYZE_CMD}" "${ANALYZE_ARGS[@]}"
|
||||
fi
|
||||
;;
|
||||
|
||||
server)
|
||||
# Server mode: Open existing project with HTTP server
|
||||
echo "Starting MCGhidra server on existing project..."
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: docker run -e MCGHIDRA_MODE=server mcghidra [program_name]"
|
||||
echo ""
|
||||
echo " program_name: Name of program in the project to open"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROGRAM_NAME="$1"
|
||||
shift
|
||||
|
||||
exec "${GHIDRA_HOME}/support/analyzeHeadless" \
|
||||
"${PROJECT_DIR}" "${PROJECT_NAME}" \
|
||||
-process "${PROGRAM_NAME}" \
|
||||
-noanalysis \
|
||||
-scriptPath "${SCRIPT_DIR}" \
|
||||
-postScript "MCGhidraServer.py" "${MCGHIDRA_PORT}" \
|
||||
"$@"
|
||||
;;
|
||||
|
||||
analyze)
|
||||
# Analyze mode: Import and analyze, then exit (no HTTP server)
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: docker run -e MCGHIDRA_MODE=analyze mcghidra [binary_path]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_PATH="$1"
|
||||
shift
|
||||
|
||||
echo "Analyzing binary: ${BINARY_PATH}"
|
||||
exec "${GHIDRA_HOME}/support/analyzeHeadless" \
|
||||
"${PROJECT_DIR}" "${PROJECT_NAME}" \
|
||||
-import "${BINARY_PATH}" \
|
||||
-max-cpu 2 \
|
||||
"$@"
|
||||
;;
|
||||
|
||||
shell)
|
||||
# Interactive shell
|
||||
exec /bin/bash
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown mode: ${MCGHIDRA_MODE}"
|
||||
echo "Valid modes: headless, server, analyze, shell"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
6
docs-site/.dockerignore
Normal file
6
docs-site/.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.astro
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
6
docs-site/.gitignore
vendored
Normal file
6
docs-site/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
61
docs-site/Dockerfile
Normal file
61
docs-site/Dockerfile
Normal file
@ -0,0 +1,61 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# -- Build stage --
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
RUN pnpm build
|
||||
|
||||
# -- Production stage --
|
||||
FROM caddy:2-alpine AS production
|
||||
|
||||
COPY --from=builder /app/dist /srv
|
||||
|
||||
COPY <<EOF /etc/caddy/Caddyfile
|
||||
:80 {
|
||||
root * /srv
|
||||
encode gzip
|
||||
try_files {path} {path}/
|
||||
file_server
|
||||
header Cache-Control "public, max-age=3600"
|
||||
@immutable path /_astro/*
|
||||
header @immutable Cache-Control "public, max-age=31536000, immutable"
|
||||
handle_errors {
|
||||
rewrite * /404.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
|
||||
# -- Dev stage --
|
||||
FROM node:22-alpine AS dev
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
13
docs-site/Makefile
Normal file
13
docs-site/Makefile
Normal file
@ -0,0 +1,13 @@
|
||||
.PHONY: prod dev down logs
|
||||
|
||||
prod:
|
||||
docker compose up -d --build
|
||||
|
||||
dev:
|
||||
docker compose --profile dev up --build
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
75
docs-site/astro.config.mjs
Normal file
75
docs-site/astro.config.mjs
Normal file
@ -0,0 +1,75 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://mcghidra.warehack.ing',
|
||||
devToolbar: { enabled: false },
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'MCGhidra',
|
||||
tagline: 'Reverse engineering bridge between Ghidra and MCP',
|
||||
description: 'Multi-instance Ghidra plugin with HATEOAS REST API and MCP server for decompilation, analysis, and binary manipulation.',
|
||||
social: [
|
||||
{ icon: 'external', label: 'Gitea', href: 'https://git.supported.systems/MCP/mcghidra' },
|
||||
{ icon: 'external', label: 'PyPI', href: 'https://pypi.org/project/mcghidra/' },
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
editLink: {
|
||||
baseUrl: 'https://git.supported.systems/MCP/mcghidra/_edit/main/docs-site/',
|
||||
},
|
||||
head: [
|
||||
{
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
name: 'theme-color',
|
||||
content: '#c85533',
|
||||
},
|
||||
},
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Overview', slug: 'getting-started/overview' },
|
||||
{ label: 'Installation', slug: 'getting-started/installation' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{ label: 'Analysis Workflows', slug: 'guides/workflows' },
|
||||
{ label: 'Cursor Pagination', slug: 'guides/cursor-pagination' },
|
||||
{ label: 'Troubleshooting', slug: 'guides/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ label: 'MCP Tools', slug: 'reference/mcp-tools' },
|
||||
{ label: 'MCP Resources', slug: 'reference/resources' },
|
||||
{ label: 'REST API', slug: 'reference/rest-api' },
|
||||
{ label: 'Configuration', slug: 'reference/configuration' },
|
||||
{ label: 'Docker Usage', slug: 'reference/docker' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Concepts',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ label: 'Architecture', slug: 'concepts/architecture' },
|
||||
{ label: 'Prior Art', slug: 'concepts/prior-art' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ label: 'Changelog', slug: 'about/changelog' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
38
docs-site/docker-compose.yml
Normal file
38
docs-site/docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
||||
services:
|
||||
docs:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy
|
||||
labels:
|
||||
caddy: ${DOMAIN:-mcghidra.l.warehack.ing}
|
||||
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
docs-dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: dev
|
||||
profiles: ["dev"]
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./public:/app/public
|
||||
- ./astro.config.mjs:/app/astro.config.mjs
|
||||
networks:
|
||||
- caddy
|
||||
labels:
|
||||
caddy: ${DOMAIN:-mcghidra.l.warehack.ing}
|
||||
caddy.reverse_proxy: "{{upstreams 4321}}"
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
24
docs-site/package.json
Normal file
24
docs-site/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "mcghidra-docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev --host",
|
||||
"start": "astro dev --host",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"@astrojs/starlight": "^0.37.4",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
4355
docs-site/pnpm-lock.yaml
generated
Normal file
4355
docs-site/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
docs-site/public/robots.txt
Normal file
2
docs-site/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
7
docs-site/src/content.config.ts
Normal file
7
docs-site/src/content.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
88
docs-site/src/content/docs/about/changelog.md
Normal file
88
docs-site/src/content/docs/about/changelog.md
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Changelog
|
||||
description: Version history and release notes
|
||||
---
|
||||
|
||||
This page summarizes each release. For full commit-level detail, see the repository history.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Symbol CRUD operations: `symbols_create`, `symbols_rename`, `symbols_delete`, `symbols_imports`, `symbols_exports`
|
||||
- Bookmark management: `bookmarks_list`, `bookmarks_create`, `bookmarks_delete`
|
||||
- Enum and typedef creation: `enums_create`, `enums_list`, `typedefs_create`, `typedefs_list`
|
||||
- Variable management: `variables_list`, `variables_rename`, `functions_variables`
|
||||
- Namespace and class tools: `namespaces_list`, `classes_list`
|
||||
- Memory segment listing: `segments_list`
|
||||
- 13 analysis prompts with progress reporting
|
||||
- Docker port auto-allocation from a configurable pool (default 8192-8223)
|
||||
- Lazy `instances_use` -- returns immediately, validates on first real call
|
||||
- All Docker operations non-blocking via thread executors
|
||||
- Session isolation for `docker_stop` and `docker_cleanup`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Eliminated 4+ hour hangs when switching to slow or unreachable instances
|
||||
- Multiple bug fixes across Docker lifecycle and session management
|
||||
|
||||
## 2025.12.1
|
||||
|
||||
### Added
|
||||
|
||||
- Cursor-based pagination with configurable `page_size` and `cursor_id`
|
||||
- Grep and regex filtering applied before pagination
|
||||
- 8 enumeration resources using `ghidra://` URIs
|
||||
|
||||
### Security
|
||||
|
||||
- ReDoS protection on regex filters
|
||||
- Session spoofing prevention for cursor operations
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Full MCP integration refactor using FastMCP
|
||||
- HATEOAS-driven API v2 with hypermedia links on all responses
|
||||
|
||||
### Added
|
||||
|
||||
- String listing across program memory
|
||||
- Data manipulation tools
|
||||
- Cross-reference analysis tools
|
||||
- Memory read and write operations
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Communication between bridge and plugin switched to structured JSON
|
||||
|
||||
### Added
|
||||
|
||||
- Test suites for bridge and plugin
|
||||
- Origin checking on HTTP requests
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Added
|
||||
|
||||
- Variable manipulation tools (rename and retype)
|
||||
- Dynamic version reporting in API responses
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- Multi-instance support -- connect to multiple Ghidra sessions and switch between them
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- Initial bridge release connecting MCP server to Ghidra plugin
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial project setup
|
||||
87
docs-site/src/content/docs/concepts/architecture.md
Normal file
87
docs-site/src/content/docs/concepts/architecture.md
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: How MCGhidra's components fit together and why
|
||||
---
|
||||
|
||||
MCGhidra is a three-layer stack. Each layer operates independently, communicates over well-defined boundaries, and can be replaced without affecting the others.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MCP Client (Claude Code, Claude Desktop, etc.) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│ stdio (MCP protocol)
|
||||
┌──────────────────────────┴──────────────────────────────────────┐
|
||||
│ MCGhidra Python Server (FastMCP) │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌────────────┐ │
|
||||
│ │Functions │ │ Data │ │Analysis │ │ Docker │ ... │
|
||||
│ │ Mixin │ │ Mixin │ │ Mixin │ │ Mixin │ │
|
||||
│ └────┬────┘ └────┬─────┘ └────┬────┘ └─────┬─────┘ │
|
||||
│ └───────────┴────────────┴─────────────┘ │
|
||||
│ HTTP Client │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│ HTTP REST (HATEOAS)
|
||||
┌──────────────────────────┴──────────────────────────────────────┐
|
||||
│ Ghidra Plugin (Java, runs inside JVM) │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ MCGhidraServer.py → HTTP endpoints │ │
|
||||
│ │ Functions / Data / Memory / Xrefs / Analysis │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ Ghidra Analysis Engine (decompiler, disassembler, types) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## The Three-Layer Stack
|
||||
|
||||
The top layer is the MCP client. Claude Code, Claude Desktop, or any MCP-compatible tool connects to MCGhidra over stdio using the Model Context Protocol. The client sees MCP tools, resources, and prompts -- it never deals with HTTP or Ghidra's internals directly.
|
||||
|
||||
The middle layer is the Python MCP server, built on FastMCP. It translates MCP tool calls into HTTP requests against the Ghidra plugin's REST API. The server is organized as a set of mixins -- Functions, Data, Analysis, Docker, and others -- each registering their own tools. This keeps the codebase navigable despite having 64+ tools.
|
||||
|
||||
The bottom layer is the Ghidra plugin. It runs as a Jython script inside Ghidra's JVM and starts an HTTP server that exposes Ghidra's analysis engine. The plugin does not know or care about MCP. It serves a HATEOAS REST API that any HTTP client can consume.
|
||||
|
||||
### Why a REST intermediary
|
||||
|
||||
A direct JVM-to-MCP bridge sounds simpler, but Ghidra's runtime imposes real constraints. The JVM uses OSGi classloading, the scripting environment is Jython (Python 2.7), and Ghidra's internal APIs are not designed for external consumption. HTTP sidesteps all of this. The Ghidra plugin speaks HTTP; the Python server speaks MCP. Each layer uses the language and runtime best suited to its job.
|
||||
|
||||
This separation also enables multi-instance support. Multiple Ghidra processes can run on different ports, each analyzing a different binary, and the MCP server routes requests to the right one. If the REST layer were baked into the MCP transport, this routing would be much harder.
|
||||
|
||||
Finally, the REST layer is language-independent. The Python server could be replaced with a Go or Rust implementation without touching the Ghidra plugin. This is not a theoretical benefit -- it means the plugin's API is usable outside of MCP entirely.
|
||||
|
||||
## HATEOAS Design
|
||||
|
||||
Most REST APIs call themselves RESTful but skip the hypermedia constraint. MCGhidra does not. Every response from the Ghidra plugin includes `_links` pointing to related resources.
|
||||
|
||||
A request to `GET /functions/0x401000` returns the function metadata along with links to decompile it, disassemble it, list its variables, and find cross-references. The client follows links rather than constructing URLs from templates.
|
||||
|
||||
This matters more for MCP agents than for human users. An agent that follows links does not need to memorize URL patterns or understand the API's URL structure upfront. It reads a response, sees what actions are available, and picks the relevant one. The API is self-describing at every step.
|
||||
|
||||
The practical effect: when the Ghidra plugin adds a new capability, the agent can discover and use it without any changes to the MCP server -- as long as the server forwards the link.
|
||||
|
||||
## Session Isolation
|
||||
|
||||
Each MCP client gets a session ID, derived from the FastMCP context. This ID scopes all stateful operations.
|
||||
|
||||
Pagination cursors are session-scoped. If two clients are paging through the same function list, their cursors are independent -- advancing one does not affect the other. Docker containers track which session started them, and `docker_stop` validates ownership before killing a container. One client cannot shut down another client's analysis session.
|
||||
|
||||
`docker_cleanup` follows the same rule. It only removes containers and port locks belonging to the calling session, unless explicitly asked to clean up orphans.
|
||||
|
||||
## Port Pooling
|
||||
|
||||
When Docker provisioning starts a new container, it needs a host port to map the container's HTTP API. Ports come from a configurable pool, defaulting to 8192-8319 (128 ports).
|
||||
|
||||
Allocation uses `flock`-based file locking. Each port has a lock file, and the allocator takes an exclusive lock before assigning it. This is safe across multiple processes -- if two MCP servers run on the same host, they will not collide.
|
||||
|
||||
The `PortPool` is lazy. It is not created until the first Docker operation that needs a port. If a user never touches Docker, the lock directory is never created and no background work occurs.
|
||||
|
||||
A background discovery thread scans the port range every 30 seconds, probing each port with a 0.5-second timeout. This is how the server finds Ghidra instances that were started outside of MCGhidra -- manually launched containers, or Ghidra instances running the plugin natively.
|
||||
|
||||
## Non-Blocking Design
|
||||
|
||||
The MCP server runs an asyncio event loop. Blocking that loop would freeze all connected clients. MCGhidra avoids this in several ways.
|
||||
|
||||
All Docker subprocess calls (`docker run`, `docker stop`, `docker logs`) run in thread pool executors via `asyncio.to_thread`. The event loop stays responsive while containers start, stop, or produce output.
|
||||
|
||||
`instances_use` is lazy. When a client switches to a new Ghidra instance, the server creates a stub immediately and returns. It does not validate the connection until the first real tool call against that instance. This avoids the situation where a slow or unreachable Ghidra instance blocks the `instances_use` call for minutes.
|
||||
|
||||
`docker_auto_start` returns as soon as the container is running. It does not wait for Ghidra to finish loading and analyzing the binary -- that can take minutes for large files. The client is expected to poll `docker_health` until the API responds.
|
||||
|
||||
The background port discovery thread runs on its own schedule and never blocks the event loop. It updates the instance list atomically, so clients always see a consistent snapshot.
|
||||
39
docs-site/src/content/docs/concepts/prior-art.md
Normal file
39
docs-site/src/content/docs/concepts/prior-art.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Prior Art
|
||||
description: Acknowledgments and related projects
|
||||
---
|
||||
|
||||
MCGhidra builds on the work of many people and projects. This page gives credit where it is due.
|
||||
|
||||
## Ghidra
|
||||
|
||||
NSA released Ghidra as open source in 2019 after years of internal development. MCGhidra would not exist without the decade of investment the agency put into building a full-featured analysis engine. The decompiler alone represents person-years of work on intermediate representations, type inference, and control flow recovery. The fact that it runs headless, supports scripting, and handles dozens of processor architectures out of the box made this project feasible.
|
||||
|
||||
Ghidra is available at [ghidra-sre.org](https://ghidra-sre.org/).
|
||||
|
||||
## GhidraMCP by Laurie Wired
|
||||
|
||||
The direct inspiration. [Laurie Wired's GhidraMCP](https://github.com/LaurieWired/GhidraMCP/) demonstrated that connecting Ghidra to the Model Context Protocol was viable and useful. MCGhidra started as a fork of her project and evolved into a different architecture -- a HATEOAS REST intermediary, multi-instance support, Docker provisioning, cursor-based pagination -- but the core idea of letting an MCP agent drive Ghidra traces back to her work. The proof of concept she built made the case that this was worth pursuing further.
|
||||
|
||||
## FastMCP
|
||||
|
||||
[FastMCP](https://github.com/jlowin/fastmcp) by Jeremiah Lowin is the Python framework that MCGhidra's MCP server is built on. Its decorator-based tool registration and mixin composition pattern made it practical to organize 64+ tools into maintainable domain modules. The `Context` system for session isolation and progress reporting is central to how MCGhidra handles multi-client scenarios. FastMCP removed a large amount of boilerplate that would otherwise dominate the codebase.
|
||||
|
||||
## HATEOAS and REST
|
||||
|
||||
The Hypermedia as the Engine of Application State constraint comes from Roy Fielding's 2000 dissertation, where he formalized the REST architectural style. Most APIs that call themselves RESTful ignore this constraint. MCGhidra embraces it because agents benefit from self-describing responses -- when every result includes `_links` to related resources, the agent does not need to memorize URL patterns or maintain a hardcoded API map.
|
||||
|
||||
## Model Context Protocol
|
||||
|
||||
Anthropic's [MCP specification](https://modelcontextprotocol.io/) provides the transport layer between MCGhidra and its clients. The protocol's tool/resource/prompt abstraction maps naturally to reverse engineering workflows: tools for mutating operations like renaming symbols, resources for read-only enumeration like listing functions, and prompts for guided analysis workflows.
|
||||
|
||||
## Related Projects
|
||||
|
||||
MCGhidra is part of a broader ecosystem of people bridging reverse engineering tools with external interfaces. Notable related work includes:
|
||||
|
||||
- Binary Ninja MCP servers that expose BN's API over the same protocol
|
||||
- IDA Pro scripting bridges that have connected IDA to external tools for years
|
||||
- Radare2 and rizin automation frameworks, which pioneered the idea of a scriptable RE command interface
|
||||
- The growing community of MCP server authors connecting domain-specific tools to language model agents
|
||||
|
||||
Each of these projects informed the design decisions in MCGhidra, whether by example or by contrast. The RE tooling community has a long history of building bridges between analysis engines and the outside world -- MCGhidra is one more entry in that tradition.
|
||||
99
docs-site/src/content/docs/getting-started/installation.md
Normal file
99
docs-site/src/content/docs/getting-started/installation.md
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Installing MCGhidra from PyPI and configuring your MCP client
|
||||
---
|
||||
|
||||
## Install from PyPI
|
||||
|
||||
```bash
|
||||
pip install mcghidra
|
||||
```
|
||||
|
||||
Or with [uv](https://docs.astral.sh/uv/) for isolated execution:
|
||||
|
||||
```bash
|
||||
uvx mcghidra
|
||||
```
|
||||
|
||||
This installs the MCP server and bundles the Ghidra plugin JAR. No separate plugin installation is needed — the server deploys the JAR automatically when starting Docker containers.
|
||||
|
||||
## MCP Client Configuration
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to your Claude Desktop configuration file:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcghidra": {
|
||||
"command": "uvx",
|
||||
"args": ["mcghidra"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add mcghidra -- uvx mcghidra
|
||||
```
|
||||
|
||||
Or if running from a local clone:
|
||||
|
||||
```bash
|
||||
claude mcp add mcghidra -- uv run --directory /path/to/MCGhidra mcghidra
|
||||
```
|
||||
|
||||
## Docker Setup (Optional)
|
||||
|
||||
If you want automatic container provisioning:
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://git.supported.systems/MCP/mcghidra.git
|
||||
cd mcghidra
|
||||
```
|
||||
|
||||
2. Build the Docker image:
|
||||
```bash
|
||||
cd docker && docker build -t mcghidra:latest -f Dockerfile ..
|
||||
```
|
||||
|
||||
Or use the MCP tool:
|
||||
```
|
||||
docker_build()
|
||||
```
|
||||
|
||||
3. The `docker_auto_start` tool handles everything else — port allocation, container naming, health polling.
|
||||
|
||||
## Analyzing Raw Firmware
|
||||
|
||||
For raw binary blobs (firmware dumps, bootloaders), specify the processor architecture:
|
||||
|
||||
```
|
||||
docker_auto_start(
|
||||
binary_path="/path/to/firmware.bin",
|
||||
language="ARM:LE:32:v4t",
|
||||
base_address="0x00000000"
|
||||
)
|
||||
```
|
||||
|
||||
Common language IDs:
|
||||
|
||||
| Target | Language ID |
|
||||
|--------|------------|
|
||||
| ARM7TDMI (Thumb) | `ARM:LE:32:v4t` |
|
||||
| ARM Cortex-M | `ARM:LE:32:Cortex` |
|
||||
| ARMv7 | `ARM:LE:32:v7` |
|
||||
| MIPS32 LE | `MIPS:LE:32:default` |
|
||||
| MIPS32 BE | `MIPS:BE:32:default` |
|
||||
| x86 32-bit | `x86:LE:32:default` |
|
||||
| x86-64 | `x86:LE:64:default` |
|
||||
|
||||
When `language` is specified, the loader is automatically set to `BinaryLoader` (raw binary import). Override with `loader="AutoImporter"` if your file has a recognized header format.
|
||||
69
docs-site/src/content/docs/getting-started/overview.md
Normal file
69
docs-site/src/content/docs/getting-started/overview.md
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Overview
|
||||
description: What MCGhidra does and how the pieces fit together
|
||||
---
|
||||
|
||||
MCGhidra is a two-part system that bridges NSA's [Ghidra](https://ghidra-sre.org/) reverse engineering framework with [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) clients.
|
||||
|
||||
## What You Get
|
||||
|
||||
- **64 MCP tools** across 12 categories (functions, data, structs, symbols, analysis, memory, variables, bookmarks, enums, namespaces, segments, Docker)
|
||||
- **13 analysis prompts** for guided RE workflows (malware triage, crypto identification, auth bypass hunting, protocol analysis, and more)
|
||||
- **19 MCP resources** for quick enumeration without tool calls
|
||||
- **Cursor-based pagination** for handling binaries with 100K+ functions
|
||||
- **Server-side grep** filtering before results hit the wire
|
||||
- **Docker provisioning** with automatic port pooling and session isolation
|
||||
|
||||
## Components
|
||||
|
||||
### Ghidra Plugin (Java)
|
||||
|
||||
A headless-compatible Ghidra script that starts an HTTP server inside the JVM. The server exposes a HATEOAS REST API — every response includes hypermedia links to related resources, so clients discover the API by following links rather than memorizing paths.
|
||||
|
||||
The plugin supports:
|
||||
- Function listing, decompilation, and disassembly
|
||||
- Memory reads and data type inspection
|
||||
- Symbol renaming and annotation
|
||||
- Cross-reference navigation
|
||||
- Program metadata and analysis status
|
||||
- Health checks for container orchestration
|
||||
|
||||
### MCP Server (Python)
|
||||
|
||||
A [FastMCP](https://github.com/jlowin/fastmcp) server that wraps the REST API as MCP tools. It adds:
|
||||
|
||||
- **Multi-instance management** — connect to multiple Ghidra sessions, switch between them
|
||||
- **Docker provisioning** — automatic container lifecycle with port pooling
|
||||
- **Raw firmware support** — specify processor language, base address, and loader for binary blobs
|
||||
- **Session isolation** — each MCP client gets its own session ID, preventing cross-talk
|
||||
|
||||
## How the Pieces Connect
|
||||
|
||||
```
|
||||
┌──────────────┐ MCP ┌──────────────┐ HTTP ┌──────────────┐
|
||||
│ MCP Client │◄────────────►│ MCGhidra │◄────────────►│ Ghidra │
|
||||
│ (Claude, │ stdio │ Python │ REST API │ Plugin │
|
||||
│ Cursor, │ │ Server │ (HATEOAS) │ (Java) │
|
||||
│ etc.) │ │ │ │ │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
│ Docker │
|
||||
│ Engine │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
The MCP client communicates with MCGhidra over stdio using the Model Context Protocol. MCGhidra translates tool calls into HTTP requests against the Ghidra plugin's REST API. When Docker is available, MCGhidra can also provision and manage Ghidra containers automatically.
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
1. **Start Ghidra** — either via Docker (`docker_auto_start`) or by running the plugin in an existing Ghidra instance
|
||||
2. **Connect** — the MCP server discovers the Ghidra instance via health checks
|
||||
3. **Analyze** — use MCP tools to decompile functions, read memory, navigate references
|
||||
4. **Iterate** — rename symbols, annotate findings, re-decompile to see updated output
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- Ghidra 11.0+ (for the plugin)
|
||||
- Docker (optional, for container management)
|
||||
158
docs-site/src/content/docs/guides/cursor-pagination.md
Normal file
158
docs-site/src/content/docs/guides/cursor-pagination.md
Normal file
@ -0,0 +1,158 @@
|
||||
---
|
||||
title: Cursor Pagination
|
||||
description: Working with large binaries using cursor-based pagination
|
||||
---
|
||||
|
||||
Large binaries can contain tens of thousands of functions, hundreds of thousands of cross-references, and thousands of strings. Returning all of that in a single tool response would overflow the MCP client's context window and produce unusable output. MCGhidra uses cursor-based pagination to deliver results in controlled pages.
|
||||
|
||||
## How it works
|
||||
|
||||
When a paginated tool returns more items than the page size, the response includes a `cursor_id`. Pass that cursor ID to `cursor_next` to get the next page. Continue until `has_more` is `false`.
|
||||
|
||||
```
|
||||
# First page: get 100 functions matching a pattern
|
||||
result = functions_list(page_size=100, grep="crypt|hash")
|
||||
# Returns:
|
||||
# {
|
||||
# result: [...],
|
||||
# pagination: {
|
||||
# cursor_id: "a1b2c3d4e5f67890",
|
||||
# total_count: 12847,
|
||||
# filtered_count: 847,
|
||||
# page_size: 100,
|
||||
# current_page: 1,
|
||||
# total_pages: 9,
|
||||
# has_more: true
|
||||
# }
|
||||
# }
|
||||
|
||||
# Next page
|
||||
result = cursor_next(cursor_id="a1b2c3d4e5f67890")
|
||||
# Returns page 2 of 9
|
||||
|
||||
# Continue until has_more is false
|
||||
result = cursor_next(cursor_id="a1b2c3d4e5f67890")
|
||||
# ...
|
||||
```
|
||||
|
||||
Each response also includes a `_message` field with a human-readable summary like "Showing 100 of 847 items (page 2/9). To get the next 100 items, call: cursor_next(cursor_id='a1b2c3d4e5f67890')". MCP clients use this to decide whether to continue fetching.
|
||||
|
||||
## Server-side grep filtering
|
||||
|
||||
The `grep` parameter filters results on the server before pagination. This is much more efficient than fetching everything and filtering client-side, because only matching items are stored in the cursor and counted toward page totals.
|
||||
|
||||
```
|
||||
# Only functions with "auth" or "login" in their name/address
|
||||
functions_list(grep="auth|login", page_size=50)
|
||||
|
||||
# Case-sensitive search (grep_ignorecase defaults to true)
|
||||
data_list_strings(grep="BEGIN CERTIFICATE", grep_ignorecase=false, page_size=50)
|
||||
```
|
||||
|
||||
The grep pattern is a regular expression. It matches against all string values in each result item -- for a function, that means the name, address, and signature fields are all searched.
|
||||
|
||||
### Pattern safety
|
||||
|
||||
Patterns are validated before execution to prevent runaway matches:
|
||||
|
||||
- Maximum 500 characters
|
||||
- Maximum 15 repetition operators (`*`, `+`, `?`, `{n,m}`)
|
||||
- Nested quantifiers like `(a+)+` are rejected
|
||||
|
||||
If a pattern fails validation, the tool returns an error with code `INVALID_GREP_PATTERN` explaining what to fix.
|
||||
|
||||
## The return_all option
|
||||
|
||||
When you need all matching results without paging through cursors, pass `return_all=True`:
|
||||
|
||||
```
|
||||
functions_list(grep="crypt", return_all=True)
|
||||
```
|
||||
|
||||
This bypasses pagination and returns every matching item in a single response. There is a token budget guard (default: 8,000 estimated tokens) that kicks in if the response would be too large. When the guard triggers, the response includes:
|
||||
|
||||
- A sample of the first 3 items
|
||||
- The available field names
|
||||
- Suggested narrower queries (grep patterns, field projections, or pagination)
|
||||
|
||||
Combine `return_all` with `grep` and `fields` to keep the response size down:
|
||||
|
||||
```
|
||||
# Get all crypto-related function names and addresses (nothing else)
|
||||
functions_list(grep="crypt|aes|sha", fields=["name", "address"], return_all=True)
|
||||
```
|
||||
|
||||
## Page size
|
||||
|
||||
The `page_size` parameter controls how many items each page contains.
|
||||
|
||||
| Parameter | Default | Maximum |
|
||||
|-----------|---------|---------|
|
||||
| `page_size` | 50 | 500 |
|
||||
|
||||
For most MCP client contexts, 50-100 items per page is a good balance between making progress and keeping individual responses readable. Going above 200 is rarely useful unless you are scripting.
|
||||
|
||||
## Cursor lifecycle
|
||||
|
||||
### TTL and eviction
|
||||
|
||||
Cursors expire after 5 minutes of inactivity (no `cursor_next` calls). The timer resets each time a cursor is accessed.
|
||||
|
||||
When more than 100 cursors exist for a session, the least-recently-used cursor is evicted to make room. In practice, you will rarely hit this limit unless you start many queries without finishing them.
|
||||
|
||||
### Session isolation
|
||||
|
||||
Each MCP client session gets its own set of cursors. You cannot access or interfere with another session's cursors. Session IDs are derived from the MCP client context -- they are not user-controllable.
|
||||
|
||||
### Management tools
|
||||
|
||||
| Tool | What it does |
|
||||
|------|-------------|
|
||||
| `cursor_list()` | Show all active cursors for the current session: IDs, page progress, TTL remaining, grep pattern |
|
||||
| `cursor_delete(cursor_id="...")` | Delete a specific cursor to free memory |
|
||||
| `cursor_delete_all()` | Delete all cursors for the current session |
|
||||
|
||||
These are useful for cleanup during long analysis sessions or when you want to re-run a query from scratch.
|
||||
|
||||
## Example: scanning all strings for credentials
|
||||
|
||||
```
|
||||
# Start with a broad credential search
|
||||
result = data_list_strings(grep="password|secret|key|token|api_key|credential", page_size=100)
|
||||
|
||||
# Process first page of results
|
||||
# ... examine the strings ...
|
||||
|
||||
# Get more if there are additional pages
|
||||
if result has cursor_id:
|
||||
result = cursor_next(cursor_id="...")
|
||||
```
|
||||
|
||||
## Example: iterating through all functions matching a pattern
|
||||
|
||||
```
|
||||
# First page
|
||||
result = functions_list(grep="handle_|process_|parse_", page_size=50)
|
||||
|
||||
# Loop through pages
|
||||
while result has cursor_id:
|
||||
# Decompile interesting functions from this page
|
||||
for func in result:
|
||||
if func looks relevant:
|
||||
functions_decompile(name=func["name"])
|
||||
|
||||
# Advance
|
||||
result = cursor_next(cursor_id="...")
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Prefer server-side `grep` over fetching everything. A query like `functions_list(grep="ssl")` is far cheaper than `functions_list(return_all=True)` followed by manual filtering.
|
||||
|
||||
- Use `fields` to reduce response size. If you only need names and addresses, `functions_list(fields=["name", "address"], page_size=100)` cuts the per-item size significantly.
|
||||
|
||||
- Small page sizes (50-100) keep individual responses from consuming too much context. You can always fetch more pages.
|
||||
|
||||
- If a cursor expires (5-minute TTL), just re-run the original query. The cursor IDs are not reusable -- you get a new one each time.
|
||||
|
||||
- For very large binaries (100K+ functions), start with grep-filtered queries rather than listing everything. Even paginated, iterating through 2,000 pages of 50 items each is slow and rarely what you actually need.
|
||||
257
docs-site/src/content/docs/guides/troubleshooting.md
Normal file
257
docs-site/src/content/docs/guides/troubleshooting.md
Normal file
@ -0,0 +1,257 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Common issues and solutions when using MCGhidra
|
||||
---
|
||||
|
||||
## Container Issues
|
||||
|
||||
### Container will not start
|
||||
|
||||
Check that the binary path is correct and accessible from the Docker daemon. The path you pass to `docker_auto_start` must exist on the host machine, and the Docker volume mount must be able to reach it.
|
||||
|
||||
```
|
||||
docker_auto_start(binary_path="/path/to/binary")
|
||||
```
|
||||
|
||||
If this fails, verify:
|
||||
|
||||
- The file exists at the specified path
|
||||
- The `mcghidra:latest` Docker image is built (run `docker_status()` to check)
|
||||
- Docker is running and your user has permission to access it
|
||||
|
||||
### Health check timeouts
|
||||
|
||||
Analysis takes time. A small binary (under 1 MB) typically finishes in about 20 seconds. Larger binaries -- especially firmware images or complex C++ programs -- can take several minutes.
|
||||
|
||||
Poll `docker_health` to check readiness:
|
||||
|
||||
```
|
||||
docker_health(port=8195)
|
||||
```
|
||||
|
||||
While waiting, check what Ghidra is doing:
|
||||
|
||||
```
|
||||
docker_logs(port=8195)
|
||||
```
|
||||
|
||||
If you see Ghidra import and analysis messages in the logs but the health check never succeeds, the analysis is still running. If the logs show errors or the container has exited, the import likely failed (see "Import failed" below).
|
||||
|
||||
### Port conflicts
|
||||
|
||||
MCGhidra allocates ports from a pool (default 8192-8319). If another application is using a port in this range, the allocator skips it. If you run many concurrent containers and exhaust the pool, `docker_auto_start` will report that no ports are available.
|
||||
|
||||
Check current allocations with:
|
||||
|
||||
```
|
||||
docker_status()
|
||||
```
|
||||
|
||||
You can adjust the port range with environment variables:
|
||||
|
||||
| Variable | Default |
|
||||
|----------|---------|
|
||||
| `MCGHIDRA_PORT_START` | `8192` |
|
||||
| `MCGHIDRA_PORT_END` | `8319` |
|
||||
|
||||
### Viewing container logs
|
||||
|
||||
```
|
||||
docker_logs(port=8195, tail=200)
|
||||
```
|
||||
|
||||
This shows stdout and stderr from the Ghidra headless process. Look for lines containing `ERROR`, `WARN`, or `Exception` to diagnose import or analysis failures.
|
||||
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### "No Ghidra instance specified"
|
||||
|
||||
This means no current instance is set. First, discover available instances, then select one:
|
||||
|
||||
```
|
||||
instances_list()
|
||||
instances_use(port=8195)
|
||||
```
|
||||
|
||||
If `instances_list` returns no instances, either no Ghidra process is running or it is on a port outside the discovery range.
|
||||
|
||||
### Instance not found after starting a container
|
||||
|
||||
`docker_auto_start` returns a port, but the MCP server does not automatically register it as the current instance. You need to call:
|
||||
|
||||
```
|
||||
instances_use(port=8195)
|
||||
```
|
||||
|
||||
If `instances_list` does not show the container, the API may not be ready yet. Poll `docker_health` first.
|
||||
|
||||
### API version mismatch
|
||||
|
||||
If you see version mismatch errors, the Ghidra plugin is older than the MCP server expects. The current server expects API v2. Update the plugin by rebuilding the Docker image or installing the latest MCGhidra release.
|
||||
|
||||
### Timeout on first tool call after instances_use
|
||||
|
||||
`instances_use` is lazy -- it creates a stub entry without connecting to Ghidra. The first real tool call (like `functions_list`) validates the connection. If Ghidra is not ready yet, that call will time out.
|
||||
|
||||
Wait for `docker_health` to report healthy before calling `instances_use`.
|
||||
|
||||
---
|
||||
|
||||
## Analysis Issues
|
||||
|
||||
### Import failed
|
||||
|
||||
Raw binaries (firmware, bootloaders) need the `language` parameter to tell Ghidra which processor architecture to use. Without it, Ghidra tries to auto-detect the format and will fail on headerless files.
|
||||
|
||||
```
|
||||
docker_auto_start(
|
||||
binary_path="/path/to/firmware.bin",
|
||||
language="ARM:LE:32:v4t",
|
||||
base_address="0x00000000"
|
||||
)
|
||||
```
|
||||
|
||||
Check the logs if auto-import fails:
|
||||
|
||||
```
|
||||
docker_logs(port=8195)
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
- Missing `language` for raw binaries
|
||||
- Incorrect base address
|
||||
- Corrupted or truncated binary file
|
||||
- Unsupported file format (check with the `file` command on the host)
|
||||
|
||||
### OSGi bundle error
|
||||
|
||||
This is a known Ghidra limitation that can occur with certain script configurations. It appears as "Failed to get OSGi bundle" in the container logs. It does not usually affect analysis results -- the API still functions. If it blocks operation, rebuilding the Docker image with the latest scripts resolves it in most cases.
|
||||
|
||||
### Analysis incomplete
|
||||
|
||||
If decompiled output looks wrong (missing function boundaries, incorrect types), Ghidra's auto-analysis may not have finished or may need a second pass:
|
||||
|
||||
```
|
||||
analysis_run()
|
||||
```
|
||||
|
||||
This triggers a full re-analysis of the current program. It can take a while on large binaries.
|
||||
|
||||
### Decompilation timeout
|
||||
|
||||
For very large or complex functions, the decompiler can take longer than the default timeout. If `functions_decompile` times out, the function may have deeply nested loops, heavy inlining, or obfuscated control flow.
|
||||
|
||||
Try disassembly instead for a faster view:
|
||||
|
||||
```
|
||||
functions_disassemble(address="00401234")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pagination Issues
|
||||
|
||||
### Cursor expired
|
||||
|
||||
Cursors have a 5-minute inactivity TTL. If you wait too long between `cursor_next` calls, the cursor is deleted. Re-run the original query to get a fresh cursor:
|
||||
|
||||
```
|
||||
functions_list(grep="crypt", page_size=100)
|
||||
```
|
||||
|
||||
See [Cursor Pagination](/guides/cursor-pagination/) for details on cursor lifecycle.
|
||||
|
||||
### Context window overflow
|
||||
|
||||
If tool responses are consuming too much context, reduce the page size:
|
||||
|
||||
```
|
||||
functions_list(page_size=25, grep="your_pattern")
|
||||
```
|
||||
|
||||
Use `fields` to limit which fields are returned:
|
||||
|
||||
```
|
||||
functions_list(page_size=50, fields=["name", "address"])
|
||||
```
|
||||
|
||||
And always prefer `grep` to filter results before they reach the client.
|
||||
|
||||
### "Session spoofing" errors
|
||||
|
||||
Session IDs are derived from the MCP client context and cannot be set manually. If you see session-related errors, it means a cursor belongs to a different MCP session. Each session (each Claude conversation, for example) has its own isolated cursor space.
|
||||
|
||||
---
|
||||
|
||||
## Docker-Specific Issues
|
||||
|
||||
### docker_auto_start appears to hang
|
||||
|
||||
`docker_auto_start` returns immediately after starting the container. It does not wait for analysis to complete. If it seems to hang, the issue is likely Docker itself taking time to pull or start the container. Check:
|
||||
|
||||
```
|
||||
docker_status()
|
||||
```
|
||||
|
||||
### Cross-session interference
|
||||
|
||||
Each MCP session has a unique session ID. Docker containers are tagged with their owning session. `docker_stop` validates that the container belongs to your session before stopping it. You cannot stop another session's container.
|
||||
|
||||
If you need to clean up containers from a previous session that is no longer active, use:
|
||||
|
||||
```
|
||||
docker_cleanup(session_only=False)
|
||||
```
|
||||
|
||||
Be careful with this -- it removes all orphaned MCGhidra containers, not just yours.
|
||||
|
||||
### Stale containers
|
||||
|
||||
If containers from previous sessions are still running, they consume ports from the pool. Use `docker_cleanup()` (which defaults to `session_only=True`) to clean up your own stale containers, or `docker_cleanup(session_only=False)` to remove all orphaned containers.
|
||||
|
||||
### Build failures
|
||||
|
||||
If `docker_build()` fails, make sure:
|
||||
|
||||
- The Dockerfile context is correct (it needs both the `docker/` directory and the project root)
|
||||
- Docker has enough disk space
|
||||
- The base Ghidra image layers download successfully (network access required for first build)
|
||||
|
||||
---
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Set the `MCGHIDRAMCP_DEBUG` environment variable before starting the MCP server to enable verbose logging:
|
||||
|
||||
```bash
|
||||
MCGHIDRAMCP_DEBUG=1 uvx mcghidra
|
||||
```
|
||||
|
||||
Or in your MCP client configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcghidra": {
|
||||
"command": "uvx",
|
||||
"args": ["mcghidra"],
|
||||
"env": {
|
||||
"MCGHIDRAMCP_DEBUG": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Debug output goes to stderr and includes:
|
||||
|
||||
- Instance discovery attempts and results
|
||||
- HTTP request/response details for Ghidra API calls
|
||||
- Cursor creation, access, and expiration events
|
||||
- Docker container lifecycle events
|
||||
- Port pool allocation and release
|
||||
|
||||
Check the MCP server's stderr output in your terminal or in the MCP client's server log viewer.
|
||||
236
docs-site/src/content/docs/guides/workflows.md
Normal file
236
docs-site/src/content/docs/guides/workflows.md
Normal file
@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Analysis Workflows
|
||||
description: Common reverse engineering workflows with MCGhidra
|
||||
---
|
||||
|
||||
These workflows assume you have MCGhidra installed and configured as described in the [Installation guide](/getting-started/installation/).
|
||||
|
||||
## Triage a Binary
|
||||
|
||||
The fastest way to get oriented in an unknown binary. Start a container, wait for Ghidra to finish analysis, then survey the surface area.
|
||||
|
||||
### 1. Start analysis
|
||||
|
||||
```
|
||||
docker_auto_start(binary_path="/path/to/target.exe")
|
||||
```
|
||||
|
||||
This returns immediately with a port number. It does not block while Ghidra runs.
|
||||
|
||||
### 2. Wait for analysis to complete
|
||||
|
||||
Poll until the HTTP API responds:
|
||||
|
||||
```
|
||||
docker_health(port=8195)
|
||||
```
|
||||
|
||||
For a small binary (under 1 MB), expect about 20 seconds. Larger binaries can take several minutes. Check `docker_logs(port=8195)` while waiting to see Ghidra's progress.
|
||||
|
||||
### 3. Set the instance as current
|
||||
|
||||
```
|
||||
instances_use(port=8195)
|
||||
```
|
||||
|
||||
After this, every tool call defaults to this instance. No need to pass `port` again.
|
||||
|
||||
### 4. Get the program overview
|
||||
|
||||
```
|
||||
program_info()
|
||||
functions_list(page_size=100)
|
||||
data_list_strings(page_size=100)
|
||||
```
|
||||
|
||||
`program_info` returns architecture, compiler, and image base address. The function and string listings give a first sense of scale and naming conventions.
|
||||
|
||||
### 5. Search for interesting patterns
|
||||
|
||||
Use server-side grep to find functions and strings related to security-sensitive behavior:
|
||||
|
||||
```
|
||||
functions_list(grep="password|key|auth|crypt|login|verify", page_size=100)
|
||||
data_list_strings(grep="password|secret|key|token|credential", page_size=100)
|
||||
```
|
||||
|
||||
From here, decompile anything that looks relevant and follow cross-references to understand the surrounding logic.
|
||||
|
||||
---
|
||||
|
||||
## Rename and Annotate Loop
|
||||
|
||||
Ghidra auto-analysis produces generic names like `FUN_00401234`. As you reverse engineer, renaming functions and adding comments makes the decompiled output progressively easier to read.
|
||||
|
||||
### 1. Decompile a function
|
||||
|
||||
```
|
||||
functions_decompile(address="00401234")
|
||||
```
|
||||
|
||||
### 2. Identify what it does
|
||||
|
||||
Read the pseudocode. Look at string references, called functions, and parameter usage to determine the function's purpose.
|
||||
|
||||
### 3. Rename it
|
||||
|
||||
```
|
||||
functions_rename(address="00401234", new_name="validate_user_credentials")
|
||||
```
|
||||
|
||||
### 4. Set the signature
|
||||
|
||||
If you can determine the parameter types and return type:
|
||||
|
||||
```
|
||||
functions_set_signature(
|
||||
address="00401234",
|
||||
signature="int validate_user_credentials(char *username, char *password)"
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Add a comment
|
||||
|
||||
```
|
||||
functions_set_comment(
|
||||
address="00401234",
|
||||
comment="Checks username/password against the SQLite user table. Returns 1 on success."
|
||||
)
|
||||
```
|
||||
|
||||
### 6. Re-decompile
|
||||
|
||||
```
|
||||
functions_decompile(address="00401234")
|
||||
```
|
||||
|
||||
The decompiled output now uses your names, types, and annotations. Functions called from `validate_user_credentials` also reflect the updated name wherever they reference it. Repeat this loop for each function you investigate.
|
||||
|
||||
---
|
||||
|
||||
## Firmware Reverse Engineering
|
||||
|
||||
Raw firmware (bootloaders, embedded system images, bare-metal code) requires extra setup because there is no ELF/PE header for Ghidra to parse.
|
||||
|
||||
### 1. Start with the right loader
|
||||
|
||||
Specify the processor language and base address:
|
||||
|
||||
```
|
||||
docker_auto_start(
|
||||
binary_path="/path/to/firmware.bin",
|
||||
language="ARM:LE:32:v4t",
|
||||
base_address="0x00000000"
|
||||
)
|
||||
```
|
||||
|
||||
When `language` is set, MCGhidra uses `BinaryLoader` to map the raw bytes at the given address. See the [Installation guide](/getting-started/installation/) for a table of common language IDs.
|
||||
|
||||
### 2. Find the entry point
|
||||
|
||||
ARM firmware typically starts with an exception vector table at address 0x00000000. The first entry is the initial stack pointer, and the second is the reset vector (entry point):
|
||||
|
||||
```
|
||||
memory_read(address="0x00000000", length=32, format="hex")
|
||||
functions_decompile(address="0x00000004")
|
||||
```
|
||||
|
||||
### 3. Identify peripherals
|
||||
|
||||
Embedded firmware talks to hardware through memory-mapped I/O. Look for reads and writes to addresses outside the firmware's code and data regions:
|
||||
|
||||
```
|
||||
data_list_strings(grep="UART|SPI|I2C|GPIO")
|
||||
functions_list(grep="init_periph|hw_init|bsp_")
|
||||
```
|
||||
|
||||
Constants like `0x40000000`, `0x48000000`, or `0xE000E000` (ARM Cortex-M NVIC) are strong indicators of peripheral access.
|
||||
|
||||
### 4. Trace interrupt handlers
|
||||
|
||||
Interrupt vector tables are typically at fixed offsets. For Cortex-M, the vector table starts at the base address. Each 4-byte entry points to a handler:
|
||||
|
||||
```
|
||||
memory_read(address="0x00000000", length=256, format="hex")
|
||||
```
|
||||
|
||||
Create functions at each non-null vector address:
|
||||
|
||||
```
|
||||
functions_create(address="0x00000040")
|
||||
functions_decompile(address="0x00000040")
|
||||
```
|
||||
|
||||
### 5. Map protocol implementations
|
||||
|
||||
Firmware that communicates over a bus (UART, SPI, USB, CAN) will have recognizable patterns: ring buffers, state machines with packet parsing, and checksum calculations. Use call graph analysis to trace from peripheral init functions to protocol handlers:
|
||||
|
||||
```
|
||||
analysis_get_callgraph(name="uart_init", max_depth=4)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Analysis Prompts
|
||||
|
||||
MCGhidra includes 13 built-in prompts that guide Claude through structured analysis workflows. Each prompt defines a series of steps, tool calls, and checks for a specific reverse engineering task.
|
||||
|
||||
### Running a prompt
|
||||
|
||||
In Claude Code or Claude Desktop, use the `/prompt` command:
|
||||
|
||||
```
|
||||
/prompt malware_triage
|
||||
```
|
||||
|
||||
Claude will then execute a multi-step analysis: listing functions, scanning strings, checking imports, and producing a structured report. Prompts that involve scanning (like `malware_triage` or `identify_crypto`) report progress as they work through each step.
|
||||
|
||||
### Available prompts
|
||||
|
||||
| Prompt | What it does |
|
||||
|--------|-------------|
|
||||
| `malware_triage` | Quick capability assessment across 21 scanning steps: checks for network activity, file manipulation, process injection, anti-analysis tricks, and persistence mechanisms |
|
||||
| `identify_crypto` | Scans for known crypto constants (AES S-boxes, SHA magic numbers), function names matching crypto libraries, and common key schedule patterns |
|
||||
| `find_authentication` | Searches for password checks, credential storage, license validation, certificate handling, and authentication bypass patterns |
|
||||
| `analyze_protocol` | Framework for reversing network or file format protocols: identifies packet structures, state machines, serialization routines |
|
||||
| `trace_data_flow` | Follows data forward or backward through a program to map how input reaches sensitive operations |
|
||||
| `find_main_logic` | Navigates past CRT startup, compiler-generated wrappers, and initialization to find the actual application entry point |
|
||||
| `analyze_imports` | Categorizes imported functions by capability (file I/O, networking, crypto, process management) and flags suspicious combinations |
|
||||
| `analyze_strings` | Groups strings by category (URLs, file paths, error messages, format strings) and cross-references them to find their usage |
|
||||
| `analyze_switch_table` | Identifies jump tables and command dispatchers, maps case values to handler functions |
|
||||
| `find_config_parsing` | Locates configuration file readers, command-line parsers, registry access, and environment variable lookups |
|
||||
| `compare_functions` | Side-by-side comparison of two functions to identify patches, variants, or shared library code |
|
||||
| `document_struct` | Traces struct usage across the binary to document field types, offsets, sizes, and purpose |
|
||||
| `find_error_handlers` | Maps error handling paths, cleanup routines, exception handlers, and exit patterns |
|
||||
|
||||
### Prompt examples
|
||||
|
||||
Triage an unknown binary for malicious capabilities:
|
||||
|
||||
```
|
||||
/prompt malware_triage
|
||||
```
|
||||
|
||||
Find all cryptographic implementations:
|
||||
|
||||
```
|
||||
/prompt identify_crypto
|
||||
```
|
||||
|
||||
Trace how user input flows to a specific sink:
|
||||
|
||||
```
|
||||
/prompt trace_data_flow
|
||||
```
|
||||
|
||||
### What happens during a prompt
|
||||
|
||||
Each prompt orchestrates a series of MCP tool calls. For example, `malware_triage` will:
|
||||
|
||||
1. Call `program_info()` to determine the architecture and format
|
||||
2. Call `functions_list(grep=...)` repeatedly with patterns for each capability category (networking, file ops, process injection, etc.)
|
||||
3. Call `data_list_strings(grep=...)` to find suspicious string patterns
|
||||
4. Call `symbols_imports(grep=...)` to categorize imported APIs
|
||||
5. Produce a summary with findings organized by risk category
|
||||
|
||||
Prompts that scan many patterns report numeric progress (e.g., "Step 12/21: Checking for anti-debug techniques") so you can see where they are in the analysis.
|
||||
67
docs-site/src/content/docs/index.mdx
Normal file
67
docs-site/src/content/docs/index.mdx
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
title: MCGhidra
|
||||
description: Reverse engineering bridge between Ghidra and MCP
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Multi-instance Ghidra with a REST API and MCP server for decompilation, analysis, and binary manipulation.
|
||||
actions:
|
||||
- text: Getting Started
|
||||
link: /getting-started/overview/
|
||||
icon: right-arrow
|
||||
- text: View on PyPI
|
||||
link: https://pypi.org/project/mcghidra/
|
||||
icon: external
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## What is MCGhidra?
|
||||
|
||||
MCGhidra connects [Ghidra](https://ghidra-sre.org/) to the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), turning Ghidra's analysis engine into a set of tools that any MCP client can use. It consists of two parts: a **Ghidra plugin** that exposes a HATEOAS REST API from within Ghidra's JVM, and a **Python MCP server** that translates those endpoints into MCP tools.
|
||||
|
||||
The result is that an MCP client can decompile functions, read memory, rename symbols, navigate cross-references, and manage Docker-based Ghidra instances — all through standard tool calls.
|
||||
|
||||
<CardGrid>
|
||||
<Card title="HATEOAS REST API" icon="puzzle">
|
||||
The Ghidra plugin serves a discoverable API with hypermedia links. Clients navigate resources by following links rather than hardcoding endpoint paths.
|
||||
</Card>
|
||||
<Card title="Multi-Instance" icon="list-format">
|
||||
Run multiple Ghidra sessions in parallel, each analyzing a different binary. The MCP server tracks instances and routes requests to the right one.
|
||||
</Card>
|
||||
<Card title="Docker Support" icon="rocket">
|
||||
Automatic container provisioning with port pooling. Pass a binary path and MCGhidra handles image building, container startup, and health polling.
|
||||
</Card>
|
||||
<Card title="Raw Firmware" icon="setting">
|
||||
Specify processor architecture, base address, and loader type for raw firmware binaries. Supports targets like ARM7TDMI, MIPS, x86, and anything Ghidra recognizes.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Quick Install
|
||||
|
||||
```bash
|
||||
# Install the MCP server
|
||||
pip install mcghidra
|
||||
|
||||
# Or use uvx for isolated execution
|
||||
uvx mcghidra
|
||||
```
|
||||
|
||||
The Ghidra plugin JAR ships inside the Python package and is automatically deployed to running Ghidra instances or Docker containers.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
MCP Client (Claude, etc.)
|
||||
│
|
||||
▼
|
||||
MCGhidra MCP Server (Python, FastMCP)
|
||||
│
|
||||
▼
|
||||
Ghidra Plugin REST API (Java, runs inside Ghidra's JVM)
|
||||
│
|
||||
▼
|
||||
Ghidra Analysis Engine
|
||||
```
|
||||
|
||||
Each layer is independently useful. The REST API works without MCP. The MCP server works without Docker. Docker support is optional for automated container management.
|
||||
115
docs-site/src/content/docs/reference/configuration.md
Normal file
115
docs-site/src/content/docs/reference/configuration.md
Normal file
@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Configuration
|
||||
description: Environment variables and settings for the MCP server, Docker containers, and port pool
|
||||
---
|
||||
|
||||
MCGhidra is configured through environment variables. No configuration file is required -- defaults work out of the box for local development.
|
||||
|
||||
## MCP Server
|
||||
|
||||
These variables control the Python MCP server process.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GHIDRA_HOST` | `localhost` | Hostname for connecting to Ghidra instances. Change this when Ghidra runs on a remote host. |
|
||||
| `MCGHIDRAMCP_DEBUG` | *unset* | Set to `1` to enable DEBUG-level logging. Shows HTTP requests, pagination details, and discovery results. |
|
||||
| `MCGHIDRA_FEEDBACK` | `true` | Enable or disable feedback collection. Set to `false` to disable. |
|
||||
| `MCGHIDRA_FEEDBACK_DB` | `~/.mcghidra/feedback.db` | Path to the SQLite database for feedback data. The parent directory is created automatically. |
|
||||
|
||||
### Internal Defaults
|
||||
|
||||
These values are set in `MCGhidraConfig` and are not currently exposed as environment variables, but can be overridden programmatically when creating the server:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `quick_discovery_range` | 18489-18498 | Port range for quick instance discovery scans |
|
||||
| `full_discovery_range` | 18400-18599 | Port range for full discovery scans (`instances_discover`) |
|
||||
| `request_timeout` | 30.0s | HTTP request timeout for Ghidra API calls |
|
||||
| `discovery_timeout` | 0.5s | HTTP timeout per port during discovery scans |
|
||||
| `default_page_size` | 50 | Default pagination page size |
|
||||
| `max_page_size` | 500 | Maximum allowed page size |
|
||||
| `cursor_ttl_seconds` | 300 | Cursor expiration time (5 minutes) |
|
||||
| `max_cursors_per_session` | 100 | Maximum active cursors per MCP session |
|
||||
| `max_response_tokens` | 8000 | Hard token budget -- the return_all guard triggers above this |
|
||||
| `expected_api_version` | 2 | Minimum API version required from the Ghidra plugin |
|
||||
|
||||
---
|
||||
|
||||
## Docker Image
|
||||
|
||||
These variables control the MCP server's Docker integration -- how it builds, tags, and starts containers.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRAMCP_VERSION` | `latest` | Docker image tag to use when starting containers. |
|
||||
| `MCGHIDRA_PORT` | `8192` | Default port for container API mapping. Overridden by auto-allocation in multi-container mode. |
|
||||
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size passed to containers. Increase for large binaries. |
|
||||
| `MCGHIDRA_DOCKER_AUTO` | `false` | When `true`, the server will automatically start a Docker container when a binary is loaded and no Ghidra instance is available. |
|
||||
|
||||
---
|
||||
|
||||
## Port Pool
|
||||
|
||||
The port pool prevents conflicts when multiple MCP sessions run containers simultaneously. Ports are allocated using `flock`-based locking.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRA_PORT_START` | `8192` | First port in the allocation pool. |
|
||||
| `MCGHIDRA_PORT_END` | `8319` | Last port in the allocation pool (128 ports total). |
|
||||
| `MCGHIDRA_PORT_LOCK_DIR` | `/tmp/mcghidra-ports` | Directory for port lock files. Created automatically on first use. |
|
||||
|
||||
Port lock files are named `port-{N}.lock` and contain JSON with the session ID, PID, and timestamp. The `docker_cleanup` tool removes stale locks from crashed processes.
|
||||
|
||||
---
|
||||
|
||||
## Container Environment
|
||||
|
||||
These variables are read by the Docker entrypoint script (`entrypoint.sh`) inside the container. They configure how Ghidra runs in headless mode.
|
||||
|
||||
### Core Settings
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRA_MODE` | `headless` | Container operating mode. See modes below. |
|
||||
| `MCGHIDRA_PORT` | `8192` | HTTP API port inside the container. The MCP server maps this to a host port from the pool. |
|
||||
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size. Passed to Ghidra's `analyzeHeadless` command. |
|
||||
|
||||
### Ghidra Paths
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GHIDRA_HOME` | `/opt/ghidra` | Ghidra installation directory inside the container. |
|
||||
| `SCRIPT_DIR` | `/home/ghidra/ghidra_scripts` | Directory for Ghidra Python scripts (MCGhidraServer.py lives here). |
|
||||
| `PROJECT_DIR` | `/projects` | Directory where Ghidra stores its project files (.gpr, .rep). |
|
||||
| `PROJECT_NAME` | `MCGhidra` | Name of the Ghidra project created for the imported binary. |
|
||||
|
||||
### Firmware Import Options
|
||||
|
||||
These are optional. When omitted, Ghidra auto-detects the binary format.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GHIDRA_LANGUAGE` | *auto-detect* | Processor language ID. Must match `ARCH:ENDIAN:SIZE:VARIANT` format (e.g., `ARM:LE:32:v4t`). Setting this causes the container to use `BinaryLoader` unless `GHIDRA_LOADER` overrides it. |
|
||||
| `GHIDRA_BASE_ADDRESS` | *auto-detect* | Base address for raw binaries. Hex format: `0x00000000` or `00000000`. |
|
||||
| `GHIDRA_LOADER` | *auto-detect* | Loader type override. Common values: `BinaryLoader` (raw bytes), `AutoImporter` (header-based detection). Must be alphanumeric with underscores. |
|
||||
|
||||
### Container Modes
|
||||
|
||||
The `MCGHIDRA_MODE` variable selects the operating mode:
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `headless` | Default. Imports the binary, runs auto-analysis, starts the HTTP API server. This is what `docker_auto_start` and `docker_start` use. |
|
||||
| `server` | Opens an existing project (no import). Requires a program name as an argument. Useful for re-analyzing a previously imported binary. |
|
||||
| `analyze` | Imports and analyzes a binary, then exits. No HTTP server. Use for batch processing. |
|
||||
| `shell` | Drops into an interactive bash shell. Useful for debugging the container environment. |
|
||||
|
||||
### Validation
|
||||
|
||||
All firmware import parameters are validated before reaching Ghidra:
|
||||
|
||||
- `GHIDRA_LANGUAGE` must match `ARCH:ENDIAN:SIZE:VARIANT` (regex-validated).
|
||||
- `GHIDRA_BASE_ADDRESS` must be valid hex, max 64-bit.
|
||||
- `GHIDRA_LOADER` must be alphanumeric with underscores.
|
||||
|
||||
Invalid values are rejected with a descriptive error before any Docker or Ghidra operations run. The MCP server validates these on the client side as well, so errors surface in tool responses rather than buried in container logs.
|
||||
87
docs-site/src/content/docs/reference/docker.md
Normal file
87
docs-site/src/content/docs/reference/docker.md
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Docker Usage
|
||||
description: Container management, port pooling, and firmware import options
|
||||
---
|
||||
|
||||
MCGhidra can automatically provision Docker containers running Ghidra in headless mode. Each container gets a dedicated port from a configurable pool, and containers are tracked by session to prevent cross-talk between concurrent MCP clients.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
docker_auto_start(binary_path="/path/to/binary")
|
||||
```
|
||||
|
||||
This checks for an existing instance analyzing the same binary, and if none is found, starts a new container with an auto-allocated port.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The Docker entrypoint accepts these environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRA_PORT` | `8192` | HTTP API port inside the container |
|
||||
| `MCGHIDRA_MAXMEM` | `2G` | Max JVM heap size |
|
||||
| `PROJECT_NAME` | `MCGhidra` | Ghidra project name |
|
||||
| `PROJECT_DIR` | `/projects` | Project directory inside container |
|
||||
| `GHIDRA_LANGUAGE` | *(auto-detect)* | Processor language ID (e.g., `ARM:LE:32:v4t`) |
|
||||
| `GHIDRA_BASE_ADDRESS` | *(auto-detect)* | Base address for raw binaries (e.g., `0x00000000`) |
|
||||
| `GHIDRA_LOADER` | *(auto-detect)* | Loader type (e.g., `BinaryLoader`) |
|
||||
|
||||
## Port Pool
|
||||
|
||||
Ports are allocated from a pool to prevent conflicts:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCGHIDRA_PORT_START` | `8192` | First port in the pool |
|
||||
| `MCGHIDRA_PORT_END` | `8319` | Last port in the pool (128 ports) |
|
||||
| `MCGHIDRA_PORT_LOCK_DIR` | `/tmp/mcghidra-ports` | Lock file directory |
|
||||
|
||||
Port allocation uses `flock`-based locking for cross-process safety.
|
||||
|
||||
## Raw Firmware Import
|
||||
|
||||
For binaries without recognized headers (raw firmware dumps, bootloader images):
|
||||
|
||||
```
|
||||
docker_start(
|
||||
binary_path="/path/to/firmware.bin",
|
||||
language="ARM:LE:32:v4t",
|
||||
base_address="0x00000000"
|
||||
)
|
||||
```
|
||||
|
||||
When `language` is specified, `loader` is automatically set to `BinaryLoader`. This tells Ghidra to treat the file as raw bytes mapped at the given base address, rather than trying to parse it as an ELF, PE, or Mach-O.
|
||||
|
||||
To override the auto-loader (e.g., if you have an ELF but need a specific language):
|
||||
|
||||
```
|
||||
docker_start(
|
||||
binary_path="/path/to/firmware.elf",
|
||||
language="ARM:LE:32:v7",
|
||||
loader="AutoImporter"
|
||||
)
|
||||
```
|
||||
|
||||
## Container Lifecycle
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `docker_auto_start` | Find existing or start new container |
|
||||
| `docker_start` | Start a container explicitly |
|
||||
| `docker_stop` | Stop and remove a container |
|
||||
| `docker_health` | Check if API is responding |
|
||||
| `docker_logs` | View container output |
|
||||
| `docker_status` | List all containers and images |
|
||||
| `docker_cleanup` | Remove orphaned containers and stale locks |
|
||||
| `docker_session_info` | Show this session's containers |
|
||||
|
||||
## Input Validation
|
||||
|
||||
All firmware import parameters are validated before reaching the container:
|
||||
|
||||
- **Language**: Must match `ARCH:ENDIAN:SIZE:VARIANT` pattern (e.g., `ARM:LE:32:v4t`)
|
||||
- **Base address**: Must be a valid hex string (e.g., `0x00000000` or `00000000`), max 64-bit
|
||||
- **Loader**: Must be alphanumeric with underscores (e.g., `BinaryLoader`)
|
||||
|
||||
Invalid values are rejected with a descriptive error before any Docker operations occur.
|
||||
742
docs-site/src/content/docs/reference/mcp-tools.md
Normal file
742
docs-site/src/content/docs/reference/mcp-tools.md
Normal file
@ -0,0 +1,742 @@
|
||||
---
|
||||
title: MCP Tools
|
||||
description: Complete reference for all MCGhidra MCP tools, grouped by domain
|
||||
---
|
||||
|
||||
MCGhidra exposes Ghidra's capabilities as MCP tools. There are 64 tools across 14 categories.
|
||||
|
||||
## Pagination Convention
|
||||
|
||||
Most list tools share a common set of pagination and filtering parameters. Rather than repeating them in every table, they are documented once here:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *current* | Ghidra instance port. Uses the active instance if omitted. |
|
||||
| `page_size` | int | `50` | Items per page. Maximum: 500. |
|
||||
| `grep` | string | *none* | Client-side regex pattern applied to results after fetching. |
|
||||
| `grep_ignorecase` | bool | `true` | Case-insensitive grep matching. |
|
||||
| `return_all` | bool | `false` | Bypass pagination and return everything. Triggers a budget guard if the response exceeds ~8000 tokens. |
|
||||
| `fields` | list[str] | *none* | Field projection -- keep only these keys per item. Reduces response size. |
|
||||
|
||||
Tools that accept these parameters are marked with "Supports pagination" below. Use `cursor_next(cursor_id)` to advance through pages.
|
||||
|
||||
---
|
||||
|
||||
## Instance Management
|
||||
|
||||
Tools for discovering, registering, and switching between Ghidra instances.
|
||||
|
||||
### `instances_list`
|
||||
|
||||
List all active Ghidra instances. Runs a quick discovery scan before returning results.
|
||||
|
||||
Returns a dict with an `instances` list containing port, URL, project, and file for each instance.
|
||||
|
||||
### `instances_use`
|
||||
|
||||
Set the current working instance. All subsequent tool calls default to this instance.
|
||||
|
||||
Uses lazy registration -- the instance is recorded immediately without a blocking HTTP call. If the instance is unreachable, the next actual tool call will fail with a clear error.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *required* | Port number of the instance to activate |
|
||||
|
||||
Returns confirmation with instance details.
|
||||
|
||||
### `instances_current`
|
||||
|
||||
Show which instance is currently active, including its port, URL, project, and file. Returns an error message with available instance ports if none is set.
|
||||
|
||||
### `instances_register`
|
||||
|
||||
Manually register an instance by port. Verifies the instance is responsive and checks API version compatibility before registering.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *required* | Port number |
|
||||
| `url` | string | *auto* | URL override (defaults to `http://{GHIDRA_HOST}:{port}`) |
|
||||
|
||||
Returns confirmation or error message.
|
||||
|
||||
### `instances_unregister`
|
||||
|
||||
Remove an instance from the registry. If the unregistered instance was the current working instance, the current selection is cleared.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *required* | Port number to unregister |
|
||||
|
||||
### `instances_discover`
|
||||
|
||||
Force a full discovery scan across the configured port range (ports 18400-18600). Use this when you need to find instances on a different host. For normal use, `instances_list` already runs a quick scan.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `host` | string | *configured* | Host to scan |
|
||||
|
||||
### `program_info`
|
||||
|
||||
Get full program metadata from the current Ghidra instance: architecture, language ID, compiler spec, image base address, and total memory size.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Functions
|
||||
|
||||
Tools for listing, decompiling, disassembling, and modifying functions. Supports pagination.
|
||||
|
||||
### `functions_list`
|
||||
|
||||
List functions with cursor-based pagination and server-side filtering. For large binaries, use `name_contains` or `name_regex` for server-side filtering before results reach the client.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name_contains` | string | *none* | Server-side substring filter (faster than grep for large binaries) |
|
||||
| `name_regex` | string | *none* | Server-side regex filter on function name |
|
||||
| `address` | string | *none* | Filter by exact function address (hex) |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `functions_get`
|
||||
|
||||
Get detailed information about a single function: name, address, signature, size, stack depth, calling convention.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Function name (mutually exclusive with address) |
|
||||
| `address` | string | *none* | Function address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `functions_decompile`
|
||||
|
||||
Decompile a function to C pseudocode. Output is split into lines for pagination -- use `grep` to search within the decompiled code.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Function name (mutually exclusive with address) |
|
||||
| `address` | string | *none* | Function address in hex |
|
||||
| `syntax_tree` | bool | `false` | Include the decompiler syntax tree (JSON) |
|
||||
| `style` | string | `"normalize"` | Decompiler simplification style |
|
||||
|
||||
Supports pagination (over decompiled lines).
|
||||
|
||||
### `functions_disassemble`
|
||||
|
||||
Get assembly-level disassembly for a function. Output is split into instruction lines for pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Function name (mutually exclusive with address) |
|
||||
| `address` | string | *none* | Function address in hex |
|
||||
|
||||
Supports pagination (over instruction lines).
|
||||
|
||||
### `functions_rename`
|
||||
|
||||
Rename a function. Identify it by current name or address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `old_name` | string | *none* | Current function name |
|
||||
| `address` | string | *none* | Function address in hex |
|
||||
| `new_name` | string | *required* | New name for the function |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `functions_set_signature`
|
||||
|
||||
Set the full prototype of a function, including return type, name, and parameter types.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Function name |
|
||||
| `address` | string | *none* | Function address in hex |
|
||||
| `signature` | string | *required* | Full signature (e.g., `"int foo(char* arg1, int arg2)"`) |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `functions_set_comment`
|
||||
|
||||
Set a decompiler-level comment on a function. Tries the function comment first, then falls back to a pre-comment if the address is not a function entry point.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address (preferably function entry point) |
|
||||
| `comment` | string | `""` | Comment text. Empty string removes the comment. |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `functions_create`
|
||||
|
||||
Create a new function definition at the specified address. Ghidra will attempt to determine the function boundaries automatically.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `functions_variables`
|
||||
|
||||
List local variables and parameters for a specific function. Supports pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Function address in hex |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
---
|
||||
|
||||
## Data
|
||||
|
||||
Tools for working with defined data items and strings.
|
||||
|
||||
### `data_list`
|
||||
|
||||
List defined data items with filtering and cursor-based pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `addr` | string | *none* | Filter by address (hex) |
|
||||
| `name` | string | *none* | Exact name match (case-sensitive) |
|
||||
| `name_contains` | string | *none* | Substring name filter (case-insensitive) |
|
||||
| `type` | string | *none* | Filter by data type (e.g., `"string"`, `"dword"`) |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `data_list_strings`
|
||||
|
||||
List all defined strings in the binary. Use `filter` for server-side content matching, or `grep` for client-side regex.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `filter` | string | *none* | Server-side string content filter |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `data_create`
|
||||
|
||||
Define a new data item at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `data_type` | string | *required* | Data type (e.g., `"string"`, `"dword"`, `"byte"`) |
|
||||
| `size` | int | *none* | Size in bytes (optional) |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `data_rename`
|
||||
|
||||
Rename a data item at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `name` | string | *required* | New name |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `data_set_type`
|
||||
|
||||
Change the data type of an existing data item.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `data_type` | string | *required* | New data type (e.g., `"uint32_t"`, `"char[10]"`) |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `data_delete`
|
||||
|
||||
Remove a data definition at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Structs
|
||||
|
||||
Tools for creating and modifying struct (composite) data types.
|
||||
|
||||
### `structs_list`
|
||||
|
||||
List all struct data types. Supports pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `category` | string | *none* | Filter by category path (e.g., `"/winapi"`) |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `structs_get`
|
||||
|
||||
Get a struct with all its fields. If the struct has more than 10 fields, the field list is paginated. Use `fields` projection to reduce response size.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Struct name |
|
||||
|
||||
Supports pagination (over struct fields).
|
||||
|
||||
### `structs_create`
|
||||
|
||||
Create a new struct data type.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Name for the struct |
|
||||
| `category` | string | *none* | Category path (e.g., `"/custom"`) |
|
||||
| `description` | string | *none* | Description text |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `structs_add_field`
|
||||
|
||||
Add a field to an existing struct. If `offset` is omitted, the field is appended to the end of the struct.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `struct_name` | string | *required* | Name of the struct |
|
||||
| `field_name` | string | *required* | Name for the new field |
|
||||
| `field_type` | string | *required* | Data type (e.g., `"int"`, `"char"`, `"pointer"`) |
|
||||
| `offset` | int | *none* | Byte offset within the struct |
|
||||
| `comment` | string | *none* | Field comment |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `structs_update_field`
|
||||
|
||||
Modify an existing field in a struct. Identify the field by name or offset. At least one of `new_name`, `new_type`, or `new_comment` must be provided.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `struct_name` | string | *required* | Name of the struct |
|
||||
| `field_name` | string | *none* | Current field name (or use `field_offset`) |
|
||||
| `field_offset` | int | *none* | Field offset (or use `field_name`) |
|
||||
| `new_name` | string | *none* | New name |
|
||||
| `new_type` | string | *none* | New data type |
|
||||
| `new_comment` | string | *none* | New comment |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `structs_delete`
|
||||
|
||||
Remove a struct data type definition.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Struct name to delete |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Symbols
|
||||
|
||||
Tools for working with the symbol table: labels, imports, and exports.
|
||||
|
||||
### `symbols_list`
|
||||
|
||||
List all symbols in the program. Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `symbols_create`
|
||||
|
||||
Create a new label/symbol at the specified address. If a symbol already exists at that address, it is renamed.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Symbol name |
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `symbols_rename`
|
||||
|
||||
Rename the primary symbol at an address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `new_name` | string | *required* | New name |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `symbols_delete`
|
||||
|
||||
Delete the primary symbol at an address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `symbols_imports`
|
||||
|
||||
List imported symbols (external references). Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `symbols_exports`
|
||||
|
||||
List exported symbols (entry points). Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
---
|
||||
|
||||
## Analysis
|
||||
|
||||
Tools for triggering and inspecting Ghidra analysis results.
|
||||
|
||||
### `analysis_run`
|
||||
|
||||
Trigger Ghidra's auto-analysis on the current program.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
| `analysis_options` | dict | *none* | Analysis options to enable/disable |
|
||||
|
||||
### `analysis_get_callgraph`
|
||||
|
||||
Generate a call graph starting from a function. Returns nodes and edges. Edges are paginated.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Starting function name |
|
||||
| `address` | string | *none* | Starting function address |
|
||||
| `max_depth` | int | `3` | Maximum call depth |
|
||||
|
||||
Supports pagination (over edges).
|
||||
|
||||
### `analysis_get_dataflow`
|
||||
|
||||
Trace data flow forward or backward from an address. Returns a list of steps showing how data propagates through the program.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Starting address in hex |
|
||||
| `direction` | string | `"forward"` | `"forward"` or `"backward"` |
|
||||
| `max_steps` | int | `50` | Maximum analysis steps |
|
||||
|
||||
Supports pagination (over steps).
|
||||
|
||||
### `xrefs_list`
|
||||
|
||||
Find cross-references to or from an address. At least one of `to_addr` or `from_addr` is required.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `to_addr` | string | *none* | Find references to this address |
|
||||
| `from_addr` | string | *none* | Find references from this address |
|
||||
| `type` | string | *none* | Filter by type: `"CALL"`, `"READ"`, `"WRITE"`, `"DATA"`, `"POINTER"` |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `comments_get`
|
||||
|
||||
Get a comment at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `comment_type` | string | `"plate"` | Type: `"plate"`, `"pre"`, `"post"`, `"eol"`, `"repeatable"` |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `comments_set`
|
||||
|
||||
Set a comment at the specified address. Pass an empty string to remove the comment.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `comment` | string | `""` | Comment text |
|
||||
| `comment_type` | string | `"plate"` | Type: `"plate"`, `"pre"`, `"post"`, `"eol"`, `"repeatable"` |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
Direct memory access tools.
|
||||
|
||||
### `memory_read`
|
||||
|
||||
Read bytes from a memory address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `length` | int | `16` | Number of bytes to read |
|
||||
| `format` | string | `"hex"` | Output format: `"hex"`, `"base64"`, or `"string"` |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
Returns the bytes in the requested format along with the actual byte count.
|
||||
|
||||
### `memory_write`
|
||||
|
||||
Write bytes to a memory address. Use with caution -- this modifies the program state.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `bytes_data` | string | *required* | Data to write |
|
||||
| `format` | string | `"hex"` | Input format: `"hex"`, `"base64"`, or `"string"` |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Variables
|
||||
|
||||
Tools for querying and renaming variables.
|
||||
|
||||
### `variables_list`
|
||||
|
||||
List variables with optional global-only filtering. Supports pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `global_only` | bool | `false` | Return only global variables |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `variables_rename`
|
||||
|
||||
Rename a variable within a function, and optionally change its data type.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `function_address` | string | *required* | Function address in hex |
|
||||
| `variable_name` | string | *required* | Current variable name |
|
||||
| `new_name` | string | *required* | New name |
|
||||
| `new_type` | string | *none* | New data type (e.g., `"int"`, `"char*"`) |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks
|
||||
|
||||
Tools for managing Ghidra bookmarks (annotations at addresses).
|
||||
|
||||
### `bookmarks_list`
|
||||
|
||||
List bookmarks with optional type and category filtering. Supports pagination.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `type` | string | *none* | Filter by type: `"Note"`, `"Warning"`, `"Error"`, `"Info"` |
|
||||
| `category` | string | *none* | Filter by category |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `bookmarks_create`
|
||||
|
||||
Create a bookmark at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `type` | string | `"Note"` | Bookmark type: `Note`, `Warning`, `Error`, `Info` |
|
||||
| `category` | string | `""` | Category string for grouping |
|
||||
| `comment` | string | `""` | Bookmark comment text |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `bookmarks_delete`
|
||||
|
||||
Delete all bookmarks at the specified address.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `address` | string | *required* | Memory address in hex |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Enums and Typedefs
|
||||
|
||||
Tools for managing enum and typedef data types.
|
||||
|
||||
### `enums_list`
|
||||
|
||||
List enum data types with their members. Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `enums_create`
|
||||
|
||||
Create a new enum data type.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Enum name |
|
||||
| `size` | int | `4` | Size in bytes |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
### `typedefs_list`
|
||||
|
||||
List typedef data types. Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `typedefs_create`
|
||||
|
||||
Create a new typedef data type.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *required* | Typedef name |
|
||||
| `base_type` | string | *required* | Base data type (e.g., `"int"`, `"uint32_t"`, `"char*"`) |
|
||||
| `port` | int | *current* | Ghidra instance port |
|
||||
|
||||
---
|
||||
|
||||
## Namespaces
|
||||
|
||||
Tools for querying namespaces and class definitions.
|
||||
|
||||
### `namespaces_list`
|
||||
|
||||
List all non-global namespaces in the program. Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
### `classes_list`
|
||||
|
||||
List class namespaces with qualified names. Supports pagination.
|
||||
|
||||
Supports pagination.
|
||||
|
||||
---
|
||||
|
||||
## Segments
|
||||
|
||||
### `segments_list`
|
||||
|
||||
List memory segments (`.text`, `.data`, `.bss`, etc.) with read/write/execute permissions, start address, and size.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | *none* | Filter by segment name (exact match) |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
---
|
||||
|
||||
## Cursors
|
||||
|
||||
Tools for managing pagination state. Every paginated tool response includes a `cursor_id` in the pagination metadata when more pages are available.
|
||||
|
||||
### `cursor_next`
|
||||
|
||||
Fetch the next page of results for a cursor. Cursors expire after 5 minutes of inactivity.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `cursor_id` | string | *required* | Cursor identifier from a previous paginated response |
|
||||
|
||||
Returns the next page of results with updated pagination info.
|
||||
|
||||
### `cursor_list`
|
||||
|
||||
List active cursors for the current session.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `all_sessions` | bool | `false` | Include cursors from all sessions |
|
||||
|
||||
### `cursor_delete`
|
||||
|
||||
Delete a specific cursor to free resources.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `cursor_id` | string | *required* | Cursor identifier to delete |
|
||||
|
||||
### `cursor_delete_all`
|
||||
|
||||
Delete all cursors for the current session.
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
Tools for managing Ghidra Docker containers. See the [Docker Usage](/reference/docker/) page for environment variables and firmware import details.
|
||||
|
||||
### `docker_auto_start`
|
||||
|
||||
The primary entry point for automatic container management. Checks all pooled ports for an existing instance analyzing the same binary. If none is found, allocates a port and starts a new container. Returns connection info immediately -- poll `docker_health` to check when the API is ready.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `binary_path` | string | *required* | Path to the binary file |
|
||||
| `language` | string | *none* | Ghidra processor language ID (e.g., `"ARM:LE:32:v4t"`) |
|
||||
| `base_address` | string | *none* | Base address for raw binaries (e.g., `"0x00000000"`) |
|
||||
| `loader` | string | *none* | Loader type. Auto-set to `"BinaryLoader"` when language is specified. |
|
||||
|
||||
### `docker_start`
|
||||
|
||||
Start a container with explicit control over all parameters. Ports are auto-allocated from the pool (8192-8319).
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `binary_path` | string | *required* | Path to the binary file |
|
||||
| `memory` | string | `"2G"` | Max JVM heap size |
|
||||
| `name` | string | *auto* | Container name (auto-generated with session ID) |
|
||||
| `language` | string | *none* | Ghidra processor language ID |
|
||||
| `base_address` | string | *none* | Base address (hex) |
|
||||
| `loader` | string | *none* | Loader type |
|
||||
|
||||
### `docker_stop`
|
||||
|
||||
Stop and optionally remove a container. Session-scoped: you can only stop containers started by your own MCP session.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name_or_id` | string | *required* | Container name or ID |
|
||||
| `remove` | bool | `true` | Also remove the container |
|
||||
|
||||
### `docker_health`
|
||||
|
||||
Check if a container's HTTP API is responding. Tries `/health` first, then falls back to the root endpoint for older plugin versions.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `port` | int | *current* | API port to check |
|
||||
| `timeout` | float | `5.0` | Request timeout in seconds |
|
||||
|
||||
### `docker_logs`
|
||||
|
||||
Get stdout/stderr from a container. Useful for monitoring analysis progress.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name_or_id` | string | *required* | Container name or ID |
|
||||
| `tail` | int | `100` | Number of lines to show |
|
||||
| `follow` | bool | `false` | Follow log output (not recommended for MCP) |
|
||||
|
||||
### `docker_status`
|
||||
|
||||
List all MCGhidra containers, Docker images, port pool allocation status, and whether Docker/Compose are available.
|
||||
|
||||
### `docker_build`
|
||||
|
||||
Build the MCGhidra Docker image from source.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `tag` | string | `"latest"` | Image tag |
|
||||
| `no_cache` | bool | `false` | Build without Docker cache |
|
||||
| `project_dir` | string | *auto* | Path to MCGhidra project root |
|
||||
|
||||
### `docker_cleanup`
|
||||
|
||||
Remove orphaned containers and stale port lock files. By default, only cleans containers from the current session for safety.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `session_only` | bool | `true` | Only clean this session's containers |
|
||||
| `max_age_hours` | float | `24.0` | Max age for orphaned containers |
|
||||
| `dry_run` | bool | `false` | Report what would be cleaned without acting |
|
||||
|
||||
### `docker_session_info`
|
||||
|
||||
Show containers and allocated ports for the current MCP session.
|
||||
203
docs-site/src/content/docs/reference/resources.md
Normal file
203
docs-site/src/content/docs/reference/resources.md
Normal file
@ -0,0 +1,203 @@
|
||||
---
|
||||
title: MCP Resources
|
||||
description: Reference for MCGhidra's read-only MCP resource URIs
|
||||
---
|
||||
|
||||
MCGhidra registers 19 MCP resources that provide read-only access to Ghidra data. Resources are a good fit for quick enumeration -- they return data without requiring tool calls and work well for populating context at the start of a conversation.
|
||||
|
||||
## Resources vs Tools
|
||||
|
||||
Resources and tools serve different purposes:
|
||||
|
||||
- **Resources** return a capped snapshot of data. They have no pagination controls, no filtering, and a fixed maximum result size. Use them for a quick overview: "what functions exist in this binary?" or "what strings are defined?"
|
||||
- **Tools** support pagination, grep filtering, field projection, and mutation operations. Use them when you need to page through large result sets, search for specific items, or modify the program.
|
||||
|
||||
If a resource hits its cap, the response includes a `_hint` field suggesting which tool to use for full pagination.
|
||||
|
||||
## Result Caps
|
||||
|
||||
Each resource type has a configurable maximum number of items it will return. These defaults are set in `MCGhidraConfig.resource_caps`:
|
||||
|
||||
| Resource Type | Default Cap |
|
||||
|---------------|-------------|
|
||||
| functions | 1000 |
|
||||
| strings | 500 |
|
||||
| data | 1000 |
|
||||
| structs | 500 |
|
||||
| xrefs | 500 |
|
||||
| symbols | 1000 |
|
||||
| segments | 500 |
|
||||
| variables | 1000 |
|
||||
| namespaces | 500 |
|
||||
| classes | 500 |
|
||||
| bookmarks | 1000 |
|
||||
| enums | 500 |
|
||||
| typedefs | 500 |
|
||||
|
||||
---
|
||||
|
||||
## Instance Resources
|
||||
|
||||
### `ghidra://instances`
|
||||
|
||||
List all active Ghidra instances. Runs a quick discovery scan before returning.
|
||||
|
||||
Returns: `instances` array (port, project, file), `count`, and `current_port`.
|
||||
|
||||
### `ghidra://instance/{port}`
|
||||
|
||||
Get detailed information about a specific Ghidra instance, including program metadata from the plugin's root endpoint.
|
||||
|
||||
**URI parameter:** `port` -- the instance port number.
|
||||
|
||||
### `ghidra://instance/{port}/summary`
|
||||
|
||||
Program overview with aggregate statistics. Fetches function count and string count in addition to basic program metadata (name, language, processor, format).
|
||||
|
||||
**URI parameter:** `port` -- the instance port number.
|
||||
|
||||
### `ghidra://instance/{port}/program`
|
||||
|
||||
Program metadata: architecture, language ID, compiler spec, image base address, and memory size. This is the same data returned by the REST API's `GET /program` endpoint.
|
||||
|
||||
**URI parameter:** `port` -- the instance port number.
|
||||
|
||||
---
|
||||
|
||||
## Function Resources
|
||||
|
||||
### `ghidra://instance/{port}/functions`
|
||||
|
||||
List functions in the program. Capped at 1000 items.
|
||||
|
||||
Returns: `functions` array, `count`, and `capped_at` (non-null if the cap was reached).
|
||||
|
||||
If capped, use the `functions_list()` tool for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/function/decompile/address/{address}`
|
||||
|
||||
Decompile a function by its address. Returns the C pseudocode as a plain text string.
|
||||
|
||||
**URI parameters:** `port`, `address` (hex, e.g., `0x401000`).
|
||||
|
||||
### `ghidra://instance/{port}/function/decompile/name/{name}`
|
||||
|
||||
Decompile a function by name. Returns the C pseudocode as a plain text string.
|
||||
|
||||
**URI parameters:** `port`, `name` (function name, e.g., `main`).
|
||||
|
||||
---
|
||||
|
||||
## Data Resources
|
||||
|
||||
### `ghidra://instance/{port}/strings`
|
||||
|
||||
List defined strings in the binary. Capped at 500 items.
|
||||
|
||||
Returns: `strings` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `data_list_strings()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/data`
|
||||
|
||||
List defined data items. Capped at 1000 items.
|
||||
|
||||
Returns: `data` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `data_list()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/structs`
|
||||
|
||||
List struct data types. Capped at 500 items.
|
||||
|
||||
Returns: `structs` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `structs_list()` for full pagination.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Reference Resources
|
||||
|
||||
### `ghidra://instance/{port}/xrefs/to/{address}`
|
||||
|
||||
Get all cross-references pointing to the specified address. Capped at 500 items.
|
||||
|
||||
**URI parameters:** `port`, `address` (hex).
|
||||
|
||||
Returns: `address`, `xrefs_to` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `xrefs_list(to_addr=...)` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/xrefs/from/{address}`
|
||||
|
||||
Get all cross-references originating from the specified address. Capped at 500 items.
|
||||
|
||||
**URI parameters:** `port`, `address` (hex).
|
||||
|
||||
Returns: `address`, `xrefs_from` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `xrefs_list(from_addr=...)` for full pagination.
|
||||
|
||||
---
|
||||
|
||||
## Symbol Resources
|
||||
|
||||
### `ghidra://instance/{port}/symbols`
|
||||
|
||||
List all symbols in the program. Capped at 1000 items.
|
||||
|
||||
Returns: `symbols` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `symbols_list()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/symbols/imports`
|
||||
|
||||
List imported symbols (external references). Capped at 1000 items.
|
||||
|
||||
Returns: `imports` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `symbols_imports()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/symbols/exports`
|
||||
|
||||
List exported symbols (entry points). Capped at 1000 items.
|
||||
|
||||
Returns: `exports` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `symbols_exports()` for full pagination.
|
||||
|
||||
---
|
||||
|
||||
## Other Resources
|
||||
|
||||
### `ghidra://instance/{port}/segments`
|
||||
|
||||
List memory segments with names, address ranges, sizes, and permissions. Capped at 500 items.
|
||||
|
||||
Returns: `segments` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `segments_list()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/namespaces`
|
||||
|
||||
List all non-global namespaces. Capped at 500 items.
|
||||
|
||||
Returns: `namespaces` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `namespaces_list()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/classes`
|
||||
|
||||
List class namespaces with qualified names. Capped at 500 items.
|
||||
|
||||
Returns: `classes` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `classes_list()` for full pagination.
|
||||
|
||||
### `ghidra://instance/{port}/variables`
|
||||
|
||||
List variables. Capped at 1000 items.
|
||||
|
||||
Returns: `variables` array, `count`, and `capped_at`.
|
||||
|
||||
If capped, use `variables_list()` for full pagination.
|
||||
423
docs-site/src/content/docs/reference/rest-api.md
Normal file
423
docs-site/src/content/docs/reference/rest-api.md
Normal file
@ -0,0 +1,423 @@
|
||||
---
|
||||
title: REST API
|
||||
description: Reference for the Ghidra plugin's HATEOAS HTTP API
|
||||
---
|
||||
|
||||
The Ghidra plugin runs an HTTP server inside the JVM and exposes a HATEOAS REST API. Every response includes hypermedia links (`_links`) to related resources, so clients can discover the API by following links rather than hardcoding paths.
|
||||
|
||||
The MCP server wraps this API as MCP tools. You generally do not need to call the REST API directly, but understanding it helps when debugging or building custom integrations.
|
||||
|
||||
## General Concepts
|
||||
|
||||
### Request Format
|
||||
|
||||
Standard HTTP verbs: `GET` to read, `POST` to create, `PATCH` to modify, `PUT` to replace, `DELETE` to remove. Request bodies use JSON (`Content-Type: application/json`). Include an `X-Request-ID` header for correlation if needed.
|
||||
|
||||
### Response Envelope
|
||||
|
||||
Every response follows this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-123",
|
||||
"instance": "http://localhost:8192",
|
||||
"success": true,
|
||||
"result": { ... },
|
||||
"_links": {
|
||||
"self": { "href": "/path/to/resource" },
|
||||
"related": { "href": "/path/to/related" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `id` -- Correlation identifier from `X-Request-ID`, or a generated value.
|
||||
- `instance` -- URL of the plugin instance that handled the request.
|
||||
- `result` -- The payload. A single object for detail endpoints, an array for list endpoints.
|
||||
- `_links` -- HATEOAS links to related resources and actions.
|
||||
|
||||
### Error Responses
|
||||
|
||||
Errors use standard HTTP status codes and include a structured error object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-456",
|
||||
"instance": "http://localhost:8192",
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "RESOURCE_NOT_FOUND",
|
||||
"message": "No function at address 0x999999"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common status codes: `200` OK, `201` Created, `400` Bad Request, `404` Not Found, `500` Internal Server Error.
|
||||
|
||||
### Pagination
|
||||
|
||||
List endpoints accept `offset` and `limit` query parameters. Responses include `size` (total count), `offset`, `limit`, and `_links` with `next`/`prev` when applicable.
|
||||
|
||||
```
|
||||
GET /functions?offset=50&limit=50
|
||||
```
|
||||
|
||||
### Addressing and Search
|
||||
|
||||
Resources can be accessed by hex address or searched by name:
|
||||
|
||||
- By address: `GET /functions/0x401000`
|
||||
- By exact name: `GET /functions?name=main`
|
||||
- By substring: `GET /functions?name_contains=init`
|
||||
- By regex: `GET /functions?name_matches_regex=^FUN_`
|
||||
|
||||
---
|
||||
|
||||
## Meta Endpoints
|
||||
|
||||
### `GET /plugin-version`
|
||||
|
||||
Returns the plugin build version and API version number. The MCP server uses this for compatibility checks.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"plugin_version": "v2.0.0",
|
||||
"api_version": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /info`
|
||||
|
||||
Returns details about the current plugin instance: loaded file, architecture, processor, address size, project name, and server port.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"file": "example.exe",
|
||||
"architecture": "x86:LE:64:default",
|
||||
"processor": "x86",
|
||||
"addressSize": 64,
|
||||
"project": "MyProject",
|
||||
"serverPort": 8192,
|
||||
"instanceCount": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /instances`
|
||||
|
||||
Lists all active plugin instances (one per open program in the Ghidra project). Each entry includes port, type, project, file, and links to connect.
|
||||
|
||||
### `GET /program`
|
||||
|
||||
Returns program metadata: language ID, compiler spec, image base address, memory size, and analysis status.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"name": "mybinary.exe",
|
||||
"languageId": "x86:LE:64:default",
|
||||
"compilerSpecId": "gcc",
|
||||
"imageBase": "0x400000",
|
||||
"memorySize": 1048576,
|
||||
"analysisComplete": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Functions
|
||||
|
||||
### `GET /functions`
|
||||
|
||||
List functions. Supports pagination and search parameters (`name`, `name_contains`, `name_matches_regex`, `addr`).
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{ "name": "main", "address": "0x401000" },
|
||||
{ "name": "init_peripherals", "address": "0x08001cf0" }
|
||||
],
|
||||
"size": 150,
|
||||
"offset": 0,
|
||||
"limit": 50
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /functions`
|
||||
|
||||
Create a function at an address. Body: `{ "address": "0x401000" }`.
|
||||
|
||||
### `GET /functions/{address}`
|
||||
|
||||
Get function details: name, signature, size, stack depth, calling convention, varargs status.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"name": "process_data",
|
||||
"address": "0x4010a0",
|
||||
"signature": "int process_data(char * data, int size)",
|
||||
"size": 128,
|
||||
"calling_convention": "__stdcall"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `PATCH /functions/{address}`
|
||||
|
||||
Modify a function. Payload can include `name`, `signature`, and `comment`.
|
||||
|
||||
```json
|
||||
{ "name": "calculate_checksum", "signature": "uint32_t calculate_checksum(uint8_t* buffer, size_t length)" }
|
||||
```
|
||||
|
||||
### `DELETE /functions/{address}`
|
||||
|
||||
Delete the function definition at the specified address.
|
||||
|
||||
### `GET /functions/{address}/decompile`
|
||||
|
||||
Get decompiled C pseudocode. Optional query parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `syntax_tree` | `true` to include the syntax tree as JSON |
|
||||
| `style` | Decompiler simplification style (e.g., `normalize`) |
|
||||
| `timeout` | Decompilation timeout in seconds |
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"address": "0x4010a0",
|
||||
"ccode": "int process_data(char *param_1, int param_2)\n{\n ...\n}\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /functions/{address}/disassembly`
|
||||
|
||||
Get assembly listing. Supports pagination (`offset`, `limit`).
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{ "address": "0x4010a0", "mnemonic": "PUSH", "operands": "RBP", "bytes": "55" },
|
||||
{ "address": "0x4010a1", "mnemonic": "MOV", "operands": "RBP, RSP", "bytes": "4889E5" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /functions/{address}/variables`
|
||||
|
||||
List local variables for a function. Supports name search.
|
||||
|
||||
### `PATCH /functions/{address}/variables/{variable_name}`
|
||||
|
||||
Modify a local variable. Payload: `{ "name": "new_name", "type": "int" }`.
|
||||
|
||||
---
|
||||
|
||||
## Data
|
||||
|
||||
### `GET /data`
|
||||
|
||||
List defined data items. Supports search (`name`, `name_contains`, `addr`, `type`) and pagination.
|
||||
|
||||
### `POST /data`
|
||||
|
||||
Define data at an address. Body: `{ "address": "0x402000", "type": "dword" }`.
|
||||
|
||||
### `GET /data/{address}`
|
||||
|
||||
Get data item details (type, size, value representation).
|
||||
|
||||
### `PATCH /data/{address}`
|
||||
|
||||
Modify a data item: change `name`, `type`, or `comment`.
|
||||
|
||||
### `DELETE /data/{address}`
|
||||
|
||||
Undefine the data item at the specified address.
|
||||
|
||||
### `GET /strings`
|
||||
|
||||
List defined strings. Supports pagination and a `filter` parameter for substring matching.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [
|
||||
{ "address": "0x00401234", "value": "Hello, world!", "length": 14, "type": "string" },
|
||||
{ "address": "0x00401250", "value": "Error: could not open file", "length": 26, "type": "string" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structs
|
||||
|
||||
### `GET /structs`
|
||||
|
||||
List struct data types. Supports pagination and `category` filtering.
|
||||
|
||||
### `GET /structs?name={name}`
|
||||
|
||||
Get detailed struct information including all fields with offsets, types, and comments.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"name": "MyStruct",
|
||||
"size": 16,
|
||||
"category": "/custom",
|
||||
"fields": [
|
||||
{ "name": "id", "offset": 0, "length": 4, "type": "int", "comment": "Unique identifier" },
|
||||
{ "name": "flags", "offset": 4, "length": 4, "type": "dword", "comment": "" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /structs/create`
|
||||
|
||||
Create a struct. Body: `{ "name": "NetworkPacket", "category": "/network" }`.
|
||||
|
||||
### `POST /structs/addfield`
|
||||
|
||||
Add a field. Body: `{ "struct": "NetworkPacket", "fieldName": "header", "fieldType": "dword" }`.
|
||||
|
||||
### `POST /structs/updatefield`
|
||||
|
||||
Update a field. Identify by `fieldName` or `fieldOffset`, then provide `newName`, `newType`, and/or `newComment`.
|
||||
|
||||
### `POST /structs/delete`
|
||||
|
||||
Delete a struct. Body: `{ "name": "NetworkPacket" }`.
|
||||
|
||||
---
|
||||
|
||||
## Symbols
|
||||
|
||||
### `GET /symbols`
|
||||
|
||||
List all symbols. Supports search and pagination. Can filter by `type` (`function`, `data`, `label`).
|
||||
|
||||
### `POST /symbols`
|
||||
|
||||
Create or rename a symbol. Body: `{ "address": "0x401000", "name": "my_label" }`.
|
||||
|
||||
### `PATCH /symbols/{address}`
|
||||
|
||||
Modify a symbol (rename, change namespace, set as primary).
|
||||
|
||||
### `DELETE /symbols/{address}`
|
||||
|
||||
Remove the symbol at the specified address.
|
||||
|
||||
---
|
||||
|
||||
## Memory
|
||||
|
||||
### `GET /memory/{address}`
|
||||
|
||||
Read bytes from memory.
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `length` | Number of bytes (required, server-imposed max) |
|
||||
| `format` | `hex`, `base64`, or `string` (default: `hex`) |
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"address": "0x402000",
|
||||
"length": 16,
|
||||
"format": "hex",
|
||||
"bytes": "48656C6C6F20576F726C642100000000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `PATCH /memory/{address}`
|
||||
|
||||
Write bytes. Body: `{ "bytes": "DEADBEEF", "format": "hex" }`. Use with caution.
|
||||
|
||||
---
|
||||
|
||||
## Segments
|
||||
|
||||
### `GET /segments`
|
||||
|
||||
List memory segments (`.text`, `.data`, `.bss`, etc.) with address ranges, sizes, and R/W/X permissions.
|
||||
|
||||
### `GET /segments/{name}`
|
||||
|
||||
Get details for a specific segment.
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
### `GET /xrefs`
|
||||
|
||||
Find cross-references. At least one query parameter is required:
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `to_addr` | References pointing to this address |
|
||||
| `from_addr` | References originating from this address |
|
||||
| `type` | Filter: `CALL`, `READ`, `WRITE`, `DATA`, `POINTER` |
|
||||
|
||||
Supports pagination.
|
||||
|
||||
---
|
||||
|
||||
## Analysis
|
||||
|
||||
### `GET /analysis`
|
||||
|
||||
Get analysis status and list of available analyzers.
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"program": "mybinary.exe",
|
||||
"analysis_enabled": true,
|
||||
"available_analyzers": [
|
||||
"Function Start Analyzer",
|
||||
"Reference Analyzer",
|
||||
"Decompiler Parameter ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /analysis`
|
||||
|
||||
Trigger re-analysis of the program.
|
||||
|
||||
### `GET /analysis/callgraph`
|
||||
|
||||
Generate a call graph.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `function` | *entry point* | Starting function name |
|
||||
| `max_depth` | `3` | Maximum call depth |
|
||||
|
||||
Returns `nodes` (functions) and `edges` (calls between them with call-site addresses).
|
||||
|
||||
### `GET /analysis/dataflow`
|
||||
|
||||
Trace data flow from an address.
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `address` | *required* | Starting address |
|
||||
| `direction` | `forward` | `forward` or `backward` |
|
||||
| `max_steps` | `50` | Maximum analysis steps |
|
||||
|
||||
Returns a list of `steps`, each with an address, instruction, and description.
|
||||
1
docs-site/src/env.d.ts
vendored
Normal file
1
docs-site/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
62
docs-site/src/styles/custom.css
Normal file
62
docs-site/src/styles/custom.css
Normal file
@ -0,0 +1,62 @@
|
||||
/* MCGhidra — reverse engineering tooling aesthetic
|
||||
* Warm amber/rust accent (#c85533), dark grays
|
||||
*/
|
||||
|
||||
/* Light mode */
|
||||
:root {
|
||||
--sl-color-accent-low: #f8e4db;
|
||||
--sl-color-accent: #b5452a;
|
||||
--sl-color-accent-high: #6e2915;
|
||||
--sl-color-white: #1a1614;
|
||||
--sl-color-gray-1: #3a3230;
|
||||
--sl-color-gray-2: #5c524e;
|
||||
--sl-color-gray-3: #8a7e78;
|
||||
--sl-color-gray-4: #b0a49e;
|
||||
--sl-color-gray-5: #d0c8c4;
|
||||
--sl-color-gray-6: #eae4e0;
|
||||
--sl-color-gray-7: #f6f2f0;
|
||||
--sl-color-black: #faf8f6;
|
||||
|
||||
--sl-font: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--sl-font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
:root[data-theme='dark'] {
|
||||
--sl-color-accent-low: #3a1a0c;
|
||||
--sl-color-accent: #e07040;
|
||||
--sl-color-accent-high: #f5c4aa;
|
||||
--sl-color-white: #f6f2f0;
|
||||
--sl-color-gray-1: #d0c8c4;
|
||||
--sl-color-gray-2: #b0a49e;
|
||||
--sl-color-gray-3: #8a7e78;
|
||||
--sl-color-gray-4: #5c524e;
|
||||
--sl-color-gray-5: #3a3230;
|
||||
--sl-color-gray-6: #261e1a;
|
||||
--sl-color-gray-7: #1a1614;
|
||||
--sl-color-black: #100c0a;
|
||||
}
|
||||
|
||||
/* Inline code background */
|
||||
:root {
|
||||
--sl-color-bg-inline-code: #eae4e0;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--sl-color-bg-inline-code: #261e1a;
|
||||
}
|
||||
|
||||
/* Tighter sidebar spacing */
|
||||
nav.sidebar .top-level > li + li {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Code blocks and disassembly tables */
|
||||
table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--sl-color-gray-6);
|
||||
font-weight: 600;
|
||||
}
|
||||
3
docs-site/tsconfig.json
Normal file
3
docs-site/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
BIN
lib/Base.jar
BIN
lib/Base.jar
Binary file not shown.
Binary file not shown.
BIN
lib/Docking.jar
BIN
lib/Docking.jar
Binary file not shown.
BIN
lib/Generic.jar
BIN
lib/Generic.jar
Binary file not shown.
BIN
lib/Project.jar
BIN
lib/Project.jar
Binary file not shown.
Binary file not shown.
BIN
lib/Utility.jar
BIN
lib/Utility.jar
Binary file not shown.
87
pom.xml
87
pom.xml
@ -4,21 +4,28 @@
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>eu.starsong.ghidra</groupId>
|
||||
<artifactId>GhydraMCP</artifactId>
|
||||
<artifactId>MCGhidra</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>dev</version>
|
||||
<name>GhydraMCP</name>
|
||||
<url>https://github.com/starsong-consulting/GhydraMCP</url>
|
||||
<name>MCGhidra</name>
|
||||
<url>https://github.com/starsong-consulting/MCGhidra</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<ghidra.jar.location>${project.basedir}/lib</ghidra.jar.location>
|
||||
<!-- Default paths to local lib - will be overridden if GHIDRA_HOME is set -->
|
||||
<ghidra.generic.jar>${project.basedir}/lib/Generic.jar</ghidra.generic.jar>
|
||||
<ghidra.softwaremodeling.jar>${project.basedir}/lib/SoftwareModeling.jar</ghidra.softwaremodeling.jar>
|
||||
<ghidra.project.jar>${project.basedir}/lib/Project.jar</ghidra.project.jar>
|
||||
<ghidra.docking.jar>${project.basedir}/lib/Docking.jar</ghidra.docking.jar>
|
||||
<ghidra.decompiler.jar>${project.basedir}/lib/Decompiler.jar</ghidra.decompiler.jar>
|
||||
<ghidra.utility.jar>${project.basedir}/lib/Utility.jar</ghidra.utility.jar>
|
||||
<ghidra.base.jar>${project.basedir}/lib/Base.jar</ghidra.base.jar>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
<maven.install.skip>true</maven.install.skip>
|
||||
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>
|
||||
<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>
|
||||
|
||||
<dependencies>
|
||||
@ -31,54 +38,55 @@
|
||||
</dependency>
|
||||
|
||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||
<!-- Paths can be overridden via GHIDRA_HOME environment variable -->
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Generic</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Generic.jar</systemPath>
|
||||
<systemPath>${ghidra.generic.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>SoftwareModeling</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/SoftwareModeling.jar</systemPath>
|
||||
<systemPath>${ghidra.softwaremodeling.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Project</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Project.jar</systemPath>
|
||||
<systemPath>${ghidra.project.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Docking</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Docking.jar</systemPath>
|
||||
<systemPath>${ghidra.docking.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Decompiler</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Decompiler.jar</systemPath>
|
||||
<systemPath>${ghidra.decompiler.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Utility</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Utility.jar</systemPath>
|
||||
<systemPath>${ghidra.utility.jar}</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Base</artifactId>
|
||||
<version>11.3.1</version>
|
||||
<version>11.4.2</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${ghidra.jar.location}/Base.jar</systemPath>
|
||||
<systemPath>${ghidra.base.jar}</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
@ -99,16 +107,6 @@
|
||||
</resources>
|
||||
<plugins>
|
||||
<!-- Set Java version -->
|
||||
<!-- Resources plugin to handle filtering -->
|
||||
<!-- <plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin> -->
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
@ -155,16 +153,16 @@
|
||||
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Implementation-Title>GhydraMCP</Implementation-Title>
|
||||
<Implementation-Title>MCGhidra</Implementation-Title>
|
||||
<Implementation-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Implementation-Version>
|
||||
<Plugin-Class>eu.starsong.ghidra.GhydraMCP</Plugin-Class>
|
||||
<Plugin-Name>GhydraMCP</Plugin-Name>
|
||||
<Plugin-Class>eu.starsong.ghidra.MCGhidra</Plugin-Class>
|
||||
<Plugin-Name>MCGhidra</Plugin-Name>
|
||||
<Plugin-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Plugin-Version>
|
||||
<Plugin-Author>LaurieWired, Teal Bauer</Plugin-Author>
|
||||
<Plugin-Description>Expose multiple Ghidra tools to MCP servers with variable management</Plugin-Description>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<finalName>GhydraMCP</finalName>
|
||||
<finalName>MCGhidra</finalName>
|
||||
<excludes>
|
||||
<exclude>**/App.class</exclude>
|
||||
</excludes>
|
||||
@ -189,7 +187,7 @@
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/ghidra-extension.xml</descriptor>
|
||||
</descriptors>
|
||||
<finalName>GhydraMCP-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
|
||||
<finalName>MCGhidra-${git.commit.id.describe}-${maven.build.timestamp}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
</execution>
|
||||
@ -205,7 +203,7 @@
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/complete-package.xml</descriptor>
|
||||
</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>
|
||||
</configuration>
|
||||
</execution>
|
||||
@ -261,6 +259,25 @@
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- Profile activated when GHIDRA_HOME environment variable is set -->
|
||||
<profile>
|
||||
<id>use-ghidra-home</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>env.GHIDRA_HOME</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<ghidra.generic.jar>${env.GHIDRA_HOME}/Ghidra/Framework/Generic/lib/Generic.jar</ghidra.generic.jar>
|
||||
<ghidra.softwaremodeling.jar>${env.GHIDRA_HOME}/Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar</ghidra.softwaremodeling.jar>
|
||||
<ghidra.project.jar>${env.GHIDRA_HOME}/Ghidra/Framework/Project/lib/Project.jar</ghidra.project.jar>
|
||||
<ghidra.docking.jar>${env.GHIDRA_HOME}/Ghidra/Framework/Docking/lib/Docking.jar</ghidra.docking.jar>
|
||||
<ghidra.decompiler.jar>${env.GHIDRA_HOME}/Ghidra/Features/Decompiler/lib/Decompiler.jar</ghidra.decompiler.jar>
|
||||
<ghidra.utility.jar>${env.GHIDRA_HOME}/Ghidra/Framework/Utility/lib/Utility.jar</ghidra.utility.jar>
|
||||
<ghidra.base.jar>${env.GHIDRA_HOME}/Ghidra/Features/Base/lib/Base.jar</ghidra.base.jar>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profile for building just the Ghidra plugin -->
|
||||
<profile>
|
||||
<id>plugin-only</id>
|
||||
|
||||
48
pyproject.toml
Normal file
48
pyproject.toml
Normal file
@ -0,0 +1,48 @@
|
||||
[project]
|
||||
name = "mcghidra"
|
||||
version = "2026.3.7"
|
||||
description = "Reverse engineering bridge: multi-instance Ghidra plugin with HATEOAS REST API and MCP server for decompilation, analysis & binary manipulation"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
authors = [
|
||||
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||
]
|
||||
dependencies = [
|
||||
"mcp>=1.22.0",
|
||||
"requests>=2.32.3",
|
||||
"fastmcp>=2.0.0",
|
||||
"fastmcp-feedback>=1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mcghidra = "mcghidra:main"
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://mcghidra.warehack.ing"
|
||||
Repository = "https://git.supported.systems/MCP/mcghidra"
|
||||
Issues = "https://git.supported.systems/MCP/mcghidra/issues"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["mcghidra"]
|
||||
|
||||
[tool.hatch.build]
|
||||
sources = ["src"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/src",
|
||||
"/README.md",
|
||||
"/pyproject.toml",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "W"]
|
||||
ignore = ["E501"] # Line too long - handled by formatter
|
||||
79
run_tests.py
79
run_tests.py
@ -1,6 +1,6 @@
|
||||
#!/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.
|
||||
"""
|
||||
import os
|
||||
@ -21,10 +21,10 @@ def run_http_api_tests():
|
||||
|
||||
# Import and run the tests
|
||||
try:
|
||||
from test_http_api import GhydraMCPHttpApiTests
|
||||
from test_http_api import MCGhidraHttpApiTests
|
||||
|
||||
# Create a test suite with all tests from GhydraMCPHttpApiTests
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(GhydraMCPHttpApiTests)
|
||||
# Create a test suite with all tests from MCGhidraHttpApiTests
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(MCGhidraHttpApiTests)
|
||||
|
||||
# Run the tests
|
||||
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
@ -68,24 +68,73 @@ def run_mcp_bridge_tests():
|
||||
print(f"Error running MCP bridge tests: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_data_tests():
|
||||
"""Run the data operations tests."""
|
||||
print_header("Running Data Operations Tests")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "test_data_operations.py"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.stdout:
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
|
||||
if result.stderr:
|
||||
print("STDERR:")
|
||||
print(result.stderr)
|
||||
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f"Error running data operations tests: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_comment_tests():
|
||||
"""Run the comment functionality tests."""
|
||||
print_header("Running Comment Tests")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "test_comments.py"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.stdout:
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
|
||||
if result.stderr:
|
||||
print("STDERR:")
|
||||
print(result.stderr)
|
||||
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f"Error running comment tests: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests"""
|
||||
print_header("GhydraMCP Test Suite")
|
||||
print_header("MCGhidra Test Suite")
|
||||
|
||||
# Run the HTTP API tests
|
||||
# Run test suites
|
||||
http_api_success = run_http_api_tests()
|
||||
|
||||
# Run the MCP bridge tests
|
||||
mcp_bridge_success = run_mcp_bridge_tests()
|
||||
data_tests_success = run_data_tests()
|
||||
comment_tests_success = run_comment_tests()
|
||||
|
||||
# Print a summary
|
||||
print_header("Test Summary")
|
||||
print(f"HTTP API Tests: {'PASSED' if http_api_success else 'FAILED'}")
|
||||
print(f"MCP Bridge Tests: {'PASSED' if mcp_bridge_success else 'FAILED'}")
|
||||
print(f"Overall: {'PASSED' if http_api_success and mcp_bridge_success else 'FAILED'}")
|
||||
print(f"Data Operations Tests: {'PASSED' if data_tests_success else 'FAILED'}")
|
||||
print(f"Comment Tests: {'PASSED' if comment_tests_success else 'FAILED'}")
|
||||
print(f"Overall: {'PASSED' if (http_api_success and mcp_bridge_success and data_tests_success and comment_tests_success) else 'FAILED'}")
|
||||
|
||||
# Return True if all tests passed, False otherwise
|
||||
return http_api_success and mcp_bridge_success
|
||||
return http_api_success and mcp_bridge_success and data_tests_success and comment_tests_success
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if we have the required dependencies
|
||||
@ -104,9 +153,15 @@ if __name__ == "__main__":
|
||||
elif sys.argv[1] == "--mcp":
|
||||
# Run only the MCP bridge tests
|
||||
success = run_mcp_bridge_tests()
|
||||
elif sys.argv[1] == "--data":
|
||||
# Run only the data operations tests
|
||||
success = run_data_tests()
|
||||
elif sys.argv[1] == "--comments":
|
||||
# Run only the comment tests
|
||||
success = run_comment_tests()
|
||||
else:
|
||||
print(f"Unknown argument: {sys.argv[1]}")
|
||||
print("Usage: python run_tests.py [--http|--mcp]")
|
||||
print("Usage: python run_tests.py [--http|--mcp|--data|--comments]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Run all tests
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
|
||||
<fileSets>
|
||||
<!-- Copy extension files to GhydraMCP/ directory -->
|
||||
<!-- Copy extension files to MCGhidra/ directory -->
|
||||
<fileSet>
|
||||
<directory>src/main/resources</directory>
|
||||
<includes>
|
||||
<include>extension.properties</include>
|
||||
<include>Module.manifest</include>
|
||||
</includes>
|
||||
<outputDirectory>GhydraMCP</outputDirectory>
|
||||
<outputDirectory>MCGhidra</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
|
||||
<dependencySets>
|
||||
<!-- Include the main project JAR as GhydraMCP.jar -->
|
||||
<!-- Include the main project JAR as MCGhidra.jar -->
|
||||
<dependencySet>
|
||||
<useProjectArtifact>true</useProjectArtifact>
|
||||
<outputDirectory>GhydraMCP/lib</outputDirectory>
|
||||
<outputFileNameMapping>GhydraMCP.jar</outputFileNameMapping>
|
||||
<outputDirectory>MCGhidra/lib</outputDirectory>
|
||||
<outputFileNameMapping>MCGhidra.jar</outputFileNameMapping>
|
||||
<unpack>false</unpack>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
|
||||
@ -39,25 +39,26 @@ import ghidra.util.Msg;
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ghidra.app.DeveloperPluginPackage.NAME,
|
||||
category = PluginCategoryNames.ANALYSIS,
|
||||
shortDescription = "GhydraMCP Plugin for AI Analysis",
|
||||
description = "Exposes program data via HATEOAS HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).",
|
||||
shortDescription = "MCGhidra Plugin for MCP Analysis",
|
||||
description = "Exposes program data via HATEOAS HTTP API for automated reverse engineering with MCP (Model Context Protocol).",
|
||||
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
|
||||
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 HttpServer server;
|
||||
private int port;
|
||||
private boolean isBaseInstance = false;
|
||||
private long serverStartTimeMs;
|
||||
|
||||
/**
|
||||
* Constructor for GhydraMCP Plugin.
|
||||
* Constructor for MCGhidra Plugin.
|
||||
* @param tool The Ghidra PluginTool
|
||||
*/
|
||||
public GhydraMCPPlugin(PluginTool tool) {
|
||||
public MCGhidraPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
|
||||
this.port = findAvailablePort();
|
||||
@ -70,8 +71,8 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
|
||||
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
|
||||
Msg.info(this, "MCGhidraPlugin loaded on port " + port);
|
||||
System.out.println("[MCGhidra] Plugin loaded on port " + port);
|
||||
|
||||
try {
|
||||
startServer();
|
||||
@ -109,11 +110,13 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
// Register Root Endpoint (should be last to include links to all other endpoints)
|
||||
registerRootEndpoint(server);
|
||||
|
||||
serverStartTimeMs = System.currentTimeMillis();
|
||||
|
||||
new Thread(() -> {
|
||||
server.start();
|
||||
Msg.info(this, "GhydraMCP HTTP server started on port " + port);
|
||||
System.out.println("[GhydraMCP] HTTP server started on port " + port);
|
||||
}, "GhydraMCP-HTTP-Server").start();
|
||||
Msg.info(this, "MCGhidra HTTP server started on port " + port);
|
||||
System.out.println("[MCGhidra] HTTP server started on port " + port);
|
||||
}, "MCGhidra-HTTP-Server").start();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,6 +142,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new DataEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new StructEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new MemoryEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
new AnalysisEndpoints(currentProgram, port, tool).registerEndpoints(server);
|
||||
@ -183,6 +187,35 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
});
|
||||
|
||||
// Health endpoint — lightweight, no program dependency
|
||||
server.createContext("/health", exchange -> {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
long uptimeMs = System.currentTimeMillis() - serverStartTimeMs;
|
||||
Program program = getCurrentProgram();
|
||||
|
||||
Map<String, Object> healthData = new HashMap<>();
|
||||
healthData.put("status", "up");
|
||||
healthData.put("port", port);
|
||||
healthData.put("api_version", ApiConstants.API_VERSION);
|
||||
healthData.put("uptime_ms", uptimeMs);
|
||||
healthData.put("program", program != null ? program.getName() : null);
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(healthData)
|
||||
.addLink("self", "/health")
|
||||
.addLink("root", "/");
|
||||
|
||||
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
|
||||
} else {
|
||||
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error handling /health", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Info endpoint
|
||||
server.createContext("/info", exchange -> {
|
||||
try {
|
||||
@ -349,7 +382,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
|
||||
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("isBaseInstance", isBaseInstance);
|
||||
|
||||
@ -358,6 +391,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
.success(true)
|
||||
.result(rootData)
|
||||
.addLink("self", "/")
|
||||
.addLink("health", "/health")
|
||||
.addLink("info", "/info")
|
||||
.addLink("plugin-version", "/plugin-version")
|
||||
.addLink("projects", "/projects")
|
||||
@ -374,7 +408,9 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
.addLink("functions", "/functions")
|
||||
.addLink("symbols", "/symbols")
|
||||
.addLink("data", "/data")
|
||||
.addLink("strings", "/strings")
|
||||
.addLink("segments", "/segments")
|
||||
.addLink("structs", "/structs")
|
||||
.addLink("memory", "/memory")
|
||||
.addLink("xrefs", "/xrefs")
|
||||
.addLink("analysis", "/analysis")
|
||||
@ -446,8 +482,8 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
public void dispose() {
|
||||
if (server != null) {
|
||||
server.stop(0); // Stop immediately
|
||||
Msg.info(this, "GhydraMCP HTTP server stopped on port " + port);
|
||||
System.out.println("[GhydraMCP] HTTP server stopped on port " + port);
|
||||
Msg.info(this, "MCGhidra HTTP server stopped on port " + port);
|
||||
System.out.println("[MCGhidra] HTTP server stopped on port " + port);
|
||||
}
|
||||
activeInstances.remove(port);
|
||||
super.dispose();
|
||||
@ -1,8 +1,8 @@
|
||||
package eu.starsong.ghidra.api;
|
||||
|
||||
public class ApiConstants {
|
||||
public static final String PLUGIN_VERSION = "v2.0.0-beta.1";
|
||||
public static final int API_VERSION = 2;
|
||||
public static final String PLUGIN_VERSION = "v2.1.0";
|
||||
public static final int API_VERSION = 2010;
|
||||
public static final int DEFAULT_PORT = 8192;
|
||||
public static final int MAX_PORT_ATTEMPTS = 10;
|
||||
}
|
||||
|
||||
@ -84,6 +84,18 @@ package eu.starsong.ghidra.endpoints;
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
server.createContext("/strings", exchange -> {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
handleListStrings(exchange);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /strings endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleData(HttpExchange exchange) throws IOException {
|
||||
@ -176,6 +188,12 @@ package eu.starsong.ghidra.endpoints;
|
||||
while (it.hasNext()) {
|
||||
Data data = it.next();
|
||||
if (block.contains(data.getAddress())) {
|
||||
// Apply addr filter if present
|
||||
String addrFilter = qparams.get("addr");
|
||||
if (addrFilter != null && !data.getAddress().toString().equals(addrFilter)) {
|
||||
continue; // Skip this data item if address doesn't match filter
|
||||
}
|
||||
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("address", data.getAddress().toString());
|
||||
item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)");
|
||||
@ -514,11 +532,16 @@ package eu.starsong.ghidra.endpoints;
|
||||
if (dataType == null) {
|
||||
throw new Exception("Could not find or parse data type: " + dataTypeStr);
|
||||
}
|
||||
|
||||
// Clear existing data
|
||||
int length = data.getLength();
|
||||
listing.clearCodeUnits(addr, addr.add(length - 1), false);
|
||||
|
||||
|
||||
// Clear existing data - need to clear enough space for the new data type
|
||||
// Use the LARGER of the old data length or new data type length
|
||||
int oldLength = data.getLength();
|
||||
int newLength = dataType.getLength();
|
||||
int lengthToClear = Math.max(oldLength, newLength > 0 ? newLength : oldLength);
|
||||
|
||||
// Clear the required space
|
||||
listing.clearCodeUnits(addr, addr.add(lengthToClear - 1), false);
|
||||
|
||||
// Create new data
|
||||
Data newData = listing.createData(addr, dataType);
|
||||
if (newData == null) {
|
||||
@ -1294,4 +1317,100 @@ package eu.starsong.ghidra.endpoints;
|
||||
}
|
||||
|
||||
// Note: The handleUpdateData method is already defined earlier in this file at line 477
|
||||
|
||||
/**
|
||||
* Handle request to list strings in the binary
|
||||
* @param exchange The HTTP exchange
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public void handleListStrings(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 2000);
|
||||
String filter = qparams.get("filter");
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> strings = new ArrayList<>();
|
||||
|
||||
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
||||
if (!block.isInitialized()) continue;
|
||||
|
||||
DataIterator it = program.getListing().getDefinedData(block.getStart(), true);
|
||||
while (it.hasNext()) {
|
||||
Data data = it.next();
|
||||
if (!block.contains(data.getAddress())) continue;
|
||||
|
||||
// Check if the data type is a string type
|
||||
String dataTypeName = data.getDataType().getName().toLowerCase();
|
||||
boolean isString = dataTypeName.contains("string") ||
|
||||
dataTypeName.contains("unicode") ||
|
||||
(dataTypeName.contains("char") && data.getLength() > 1); // Array of chars
|
||||
|
||||
if (isString) {
|
||||
// Get the string value
|
||||
String value = data.getDefaultValueRepresentation();
|
||||
if (value == null) value = "";
|
||||
|
||||
// Skip if it doesn't match the filter
|
||||
if (filter != null && !filter.isEmpty() && !value.toLowerCase().contains(filter.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> stringInfo = new HashMap<>();
|
||||
stringInfo.put("address", data.getAddress().toString());
|
||||
stringInfo.put("value", value);
|
||||
stringInfo.put("length", data.getLength());
|
||||
stringInfo.put("type", data.getDataType().getName());
|
||||
|
||||
// If the data has a label/name, include it
|
||||
String name = null;
|
||||
Symbol symbol = program.getSymbolTable().getPrimarySymbol(data.getAddress());
|
||||
if (symbol != null) {
|
||||
name = symbol.getName();
|
||||
}
|
||||
stringInfo.put("name", name != null ? name : "");
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/data/" + data.getAddress().toString());
|
||||
links.put("self", selfLink);
|
||||
|
||||
Map<String, String> memoryLink = new HashMap<>();
|
||||
memoryLink.put("href", "/memory?address=" + data.getAddress().toString());
|
||||
links.put("memory", memoryLink);
|
||||
|
||||
stringInfo.put("_links", links);
|
||||
|
||||
strings.add(stringInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with HATEOAS links
|
||||
eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port)
|
||||
.success(true);
|
||||
|
||||
// Apply pagination and get paginated items
|
||||
List<Map<String, Object>> paginated = applyPagination(strings, offset, limit, builder, "/strings");
|
||||
|
||||
// Set the paginated result
|
||||
builder.result(paginated);
|
||||
|
||||
// Add program link
|
||||
builder.addLink("program", "/program");
|
||||
builder.addLink("data", "/data");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error listing strings", e);
|
||||
sendErrorResponse(exchange, 500, "Error listing strings: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,15 +7,24 @@ import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import eu.starsong.ghidra.model.FunctionInfo;
|
||||
import eu.starsong.ghidra.util.GhidraUtil;
|
||||
import eu.starsong.ghidra.util.TransactionHelper;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressFactory;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.pcode.HighFunction;
|
||||
import ghidra.program.model.pcode.HighFunctionDBUtil;
|
||||
import ghidra.program.model.pcode.HighSymbol;
|
||||
import ghidra.program.model.symbol.SourceType;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.io.IOException;
|
||||
@ -500,12 +509,39 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if address is in a valid memory block
|
||||
if (program.getMemory().getBlock(address) == null) {
|
||||
sendErrorResponse(exchange, 400, "Address is not in a defined memory block: " + addressStr, "INVALID_ADDRESS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if function already exists
|
||||
if (program.getFunctionManager().getFunctionAt(address) != null) {
|
||||
sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to disassemble the code at the specified address before creating a function
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> {
|
||||
// Check if there's already a defined instruction at the address
|
||||
if (program.getListing().getInstructionAt(address) == null) {
|
||||
// Attempt to directly disassemble at the address
|
||||
try {
|
||||
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
|
||||
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
|
||||
cmd.applyTo(program);
|
||||
} catch (Exception ex) {
|
||||
Msg.warn(this, "Basic disassembly failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Log the error but proceed with function creation attempt anyway
|
||||
Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Create function
|
||||
Function function;
|
||||
try {
|
||||
@ -513,8 +549,31 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
return program.getFunctionManager().createFunction(null, address, null, null);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Failed to create function: " + e.getMessage(), "CREATE_FAILED");
|
||||
return;
|
||||
// If function creation initially fails, try a different approach
|
||||
try {
|
||||
Msg.info(this, "Initial function creation failed, attempting with code unit clearing");
|
||||
|
||||
// Clear any existing data at this location and try disassembling again
|
||||
TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> {
|
||||
// Clear existing data at the address
|
||||
program.getListing().clearCodeUnits(address, address, false);
|
||||
|
||||
// Try disassembling again
|
||||
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
|
||||
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
|
||||
cmd.applyTo(program);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Try creating the function again
|
||||
function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> {
|
||||
return program.getFunctionManager().createFunction(null, address, null, null);
|
||||
});
|
||||
} catch (Exception e2) {
|
||||
// Both attempts failed, return the error
|
||||
sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
@ -747,6 +806,14 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
handleDisassembleFunction(exchange, function);
|
||||
} else if (resource.equals("variables")) {
|
||||
handleFunctionVariables(exchange, function);
|
||||
} else if (resource.startsWith("variables/")) {
|
||||
// Handle variable operations
|
||||
String variableName = resource.substring("variables/".length());
|
||||
if ("PATCH".equals(exchange.getRequestMethod())) {
|
||||
handleUpdateVariable(exchange, function, variableName);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method not allowed for variable operations", "METHOD_NOT_ALLOWED");
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND");
|
||||
}
|
||||
@ -939,6 +1006,27 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to disassemble the code at the specified address before creating a function
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> {
|
||||
// Check if there's already a defined instruction at the address
|
||||
if (program.getListing().getInstructionAt(address) == null) {
|
||||
// Attempt to directly disassemble at the address
|
||||
try {
|
||||
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
|
||||
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
|
||||
cmd.applyTo(program);
|
||||
} catch (Exception ex) {
|
||||
Msg.warn(this, "Basic disassembly failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Log the error but proceed with function creation attempt anyway
|
||||
Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Create function
|
||||
Function function;
|
||||
@ -947,8 +1035,31 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
return program.getFunctionManager().createFunction(null, address, null, null);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Failed to create function: " + e.getMessage(), "CREATE_FAILED");
|
||||
return;
|
||||
// If function creation initially fails, try a different approach
|
||||
try {
|
||||
Msg.info(this, "Initial function creation failed, attempting with code unit clearing");
|
||||
|
||||
// Clear any existing data at this location and try disassembling again
|
||||
TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> {
|
||||
// Clear existing data at the address
|
||||
program.getListing().clearCodeUnits(address, address, false);
|
||||
|
||||
// Try disassembling again
|
||||
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
|
||||
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
|
||||
cmd.applyTo(program);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Try creating the function again
|
||||
function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> {
|
||||
return program.getFunctionManager().createFunction(null, address, null, null);
|
||||
});
|
||||
} catch (Exception e2) {
|
||||
// Both attempts failed, return the error
|
||||
sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
@ -979,20 +1090,67 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
String style = params.getOrDefault("style", "normalize");
|
||||
String format = params.getOrDefault("format", "structured");
|
||||
int timeout = parseIntOrDefault(params.get("timeout"), 30);
|
||||
|
||||
|
||||
// Line filtering parameters for context management
|
||||
int startLine = parseIntOrDefault(params.get("start_line"), -1);
|
||||
int endLine = parseIntOrDefault(params.get("end_line"), -1);
|
||||
int maxLines = parseIntOrDefault(params.get("max_lines"), -1);
|
||||
|
||||
// Decompile function
|
||||
String decompilation = GhidraUtil.decompileFunction(function);
|
||||
|
||||
|
||||
// Apply line filtering if requested
|
||||
String filteredDecompilation = decompilation;
|
||||
int totalLines = 0;
|
||||
if (decompilation != null) {
|
||||
String[] lines = decompilation.split("\n");
|
||||
totalLines = lines.length;
|
||||
|
||||
// Apply line range filtering
|
||||
if (startLine > 0 || endLine > 0 || maxLines > 0) {
|
||||
int start = startLine > 0 ? Math.max(0, startLine - 1) : 0;
|
||||
int end = endLine > 0 ? Math.min(lines.length, endLine) : lines.length;
|
||||
|
||||
// If maxLines is specified, limit the range
|
||||
if (maxLines > 0) {
|
||||
end = Math.min(end, start + maxLines);
|
||||
}
|
||||
|
||||
if (start < lines.length) {
|
||||
StringBuilder filtered = new StringBuilder();
|
||||
for (int i = start; i < end && i < lines.length; i++) {
|
||||
if (i > start) {
|
||||
filtered.append("\n");
|
||||
}
|
||||
filtered.append(lines[i]);
|
||||
}
|
||||
filteredDecompilation = filtered.toString();
|
||||
} else {
|
||||
filteredDecompilation = "// No lines in specified range";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create function info
|
||||
Map<String, Object> functionInfo = new HashMap<>();
|
||||
functionInfo.put("address", function.getEntryPoint().toString());
|
||||
functionInfo.put("name", function.getName());
|
||||
|
||||
|
||||
// Create the result structure according to GHIDRA_HTTP_API.md
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("function", functionInfo);
|
||||
result.put("decompiled", decompilation != null ? decompilation : "// Decompilation failed");
|
||||
|
||||
result.put("decompiled", filteredDecompilation != null ? filteredDecompilation : "// Decompilation failed");
|
||||
|
||||
// Add metadata about line filtering if applied
|
||||
if (startLine > 0 || endLine > 0 || maxLines > 0) {
|
||||
Map<String, Object> filterInfo = new HashMap<>();
|
||||
filterInfo.put("total_lines", totalLines);
|
||||
if (startLine > 0) filterInfo.put("start_line", startLine);
|
||||
if (endLine > 0) filterInfo.put("end_line", endLine);
|
||||
if (maxLines > 0) filterInfo.put("max_lines", maxLines);
|
||||
result.put("filter", filterInfo);
|
||||
}
|
||||
|
||||
// Add syntax tree if requested
|
||||
if (syntaxTree) {
|
||||
result.put("syntax_tree", "Syntax tree not implemented");
|
||||
@ -1163,8 +1321,75 @@ public class FunctionEndpoints extends AbstractEndpoint {
|
||||
* Handle requests to update a function variable
|
||||
*/
|
||||
private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException {
|
||||
// This is a placeholder - actual implementation would update the variable
|
||||
sendErrorResponse(exchange, 501, "Variable update not implemented", "NOT_IMPLEMENTED");
|
||||
try {
|
||||
// Parse the request body to get the update parameters
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
String newName = params.get("name");
|
||||
String newDataType = params.get("data_type");
|
||||
|
||||
if (newName == null && newDataType == null) {
|
||||
sendErrorResponse(exchange, 400, "Missing update parameters - name or data_type required", "MISSING_PARAMETER");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use transaction to update variable
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean success = TransactionHelper.executeInTransaction(program, "Update Variable", () -> {
|
||||
try {
|
||||
// This requires a decompile operation to get the HighFunction
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
try {
|
||||
decomp.openProgram(program);
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
|
||||
if (results.decompileCompleted()) {
|
||||
HighFunction highFunc = results.getHighFunction();
|
||||
if (highFunc != null) {
|
||||
// Find the variable in the high function
|
||||
for (Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols(); symbolIter.hasNext();) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.getName().equals(variableName)) {
|
||||
// Rename the variable using HighFunctionDBUtil
|
||||
HighFunctionDBUtil.updateDBVariable(symbol, newName, null, SourceType.USER_DEFINED);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
decomp.dispose();
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error updating variable: " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (success) {
|
||||
// Create a successful response
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("name", newName != null ? newName : variableName);
|
||||
result.put("function", function.getName());
|
||||
result.put("address", function.getEntryPoint().toString());
|
||||
result.put("message", "Variable renamed successfully");
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(result);
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 404, "Function resource not found: variables/" + variableName, "RESOURCE_NOT_FOUND");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 500, "Error processing variable update request: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,7 +4,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import eu.starsong.ghidra.GhydraMCPPlugin; // Need access to activeInstances
|
||||
import eu.starsong.ghidra.MCGhidraPlugin; // Need access to activeInstances
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
@ -13,16 +13,16 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
public class InstanceEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Need a way to access the static activeInstances map from GhydraMCPPlugin
|
||||
// Need a way to access the static activeInstances map from MCGhidraPlugin
|
||||
// This is a bit awkward and suggests the instance management might need
|
||||
// a different design, perhaps a dedicated manager class.
|
||||
// For now, we pass the map or use a static accessor if made public.
|
||||
private final Map<Integer, GhydraMCPPlugin> activeInstances;
|
||||
private final Map<Integer, MCGhidraPlugin> activeInstances;
|
||||
// Note: Passing currentProgram might be null here if no program is open.
|
||||
// The constructor in AbstractEndpoint handles null program.
|
||||
|
||||
// Updated constructor to accept port
|
||||
public InstanceEndpoints(Program program, int port, Map<Integer, GhydraMCPPlugin> instances) {
|
||||
public InstanceEndpoints(Program program, int port, Map<Integer, MCGhidraPlugin> instances) {
|
||||
super(program, port); // Call super constructor
|
||||
this.activeInstances = instances;
|
||||
}
|
||||
@ -46,7 +46,7 @@ package eu.starsong.ghidra.endpoints;
|
||||
|
||||
// Accessing the static map directly - requires it to be accessible
|
||||
// or passed in constructor.
|
||||
for (Map.Entry<Integer, GhydraMCPPlugin> entry : activeInstances.entrySet()) {
|
||||
for (Map.Entry<Integer, MCGhidraPlugin> entry : activeInstances.entrySet()) {
|
||||
Map<String, Object> instance = new HashMap<>();
|
||||
int instancePort = entry.getKey();
|
||||
instance.put("port", instancePort);
|
||||
|
||||
@ -1477,25 +1477,48 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
|
||||
try {
|
||||
Map<String, String> params = parseQueryParams(exchange);
|
||||
String functionName = params.get("function");
|
||||
// Support both function name and address as separate parameters
|
||||
String name = params.get("name");
|
||||
String address = params.get("address");
|
||||
// For backward compatibility, also check "function" parameter
|
||||
if (name == null) {
|
||||
name = params.get("function");
|
||||
}
|
||||
int maxDepth = parseIntOrDefault(params.get("max_depth"), 3);
|
||||
|
||||
// Get starting function
|
||||
ghidra.program.model.listing.Function startFunction = null;
|
||||
if (functionName != null) {
|
||||
|
||||
// Try to find function by address first (if provided)
|
||||
if (address != null) {
|
||||
try {
|
||||
ghidra.program.model.address.Address addr = program.getAddressFactory().getAddress(address);
|
||||
startFunction = program.getFunctionManager().getFunctionAt(addr);
|
||||
|
||||
if (startFunction == null) {
|
||||
sendErrorResponse(exchange, 404, "Function not found at address: " + address, "FUNCTION_NOT_FOUND");
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid address format: " + address, "INVALID_ADDRESS");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Try to find function by name if address not provided or function not found
|
||||
else if (name != null) {
|
||||
for (ghidra.program.model.listing.Function f : program.getFunctionManager().getFunctions(true)) {
|
||||
if (f.getName().equals(functionName)) {
|
||||
if (f.getName().equals(name)) {
|
||||
startFunction = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startFunction == null) {
|
||||
sendErrorResponse(exchange, 404, "Function not found: " + functionName, "FUNCTION_NOT_FOUND");
|
||||
sendErrorResponse(exchange, 404, "Function not found by name: " + name, "FUNCTION_NOT_FOUND");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Use the entry point function if no function is specified
|
||||
// Use the entry point function if no function is specified by name or address
|
||||
ghidra.program.model.address.Address entryPoint = program.getSymbolTable().getExternalEntryPointIterator().hasNext() ?
|
||||
program.getSymbolTable().getExternalEntryPointIterator().next() :
|
||||
program.getImageBase();
|
||||
@ -1518,10 +1541,22 @@ public class ProgramEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Add HATEOAS links
|
||||
StringBuilder selfLinkBuilder = new StringBuilder("/programs/current/analysis/callgraph");
|
||||
if (functionName != null) {
|
||||
selfLinkBuilder.append("?function=").append(functionName);
|
||||
boolean hasParam = false;
|
||||
|
||||
// Add appropriate parameters to self link
|
||||
if (address != null) {
|
||||
selfLinkBuilder.append("?address=").append(address);
|
||||
hasParam = true;
|
||||
} else if (name != null) {
|
||||
selfLinkBuilder.append("?name=").append(name);
|
||||
hasParam = true;
|
||||
}
|
||||
|
||||
if (hasParam) {
|
||||
selfLinkBuilder.append("&max_depth=").append(maxDepth);
|
||||
} else {
|
||||
selfLinkBuilder.append("?max_depth=").append(maxDepth);
|
||||
}
|
||||
selfLinkBuilder.append("&max_depth=").append(maxDepth);
|
||||
|
||||
builder.addLink("self", selfLinkBuilder.toString());
|
||||
builder.addLink("program", "/programs/current");
|
||||
|
||||
776
src/main/java/eu/starsong/ghidra/endpoints/StructEndpoints.java
Normal file
776
src/main/java/eu/starsong/ghidra/endpoints/StructEndpoints.java
Normal file
@ -0,0 +1,776 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder;
|
||||
import eu.starsong.ghidra.util.TransactionHelper;
|
||||
import eu.starsong.ghidra.util.TransactionHelper.TransactionException;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.data.*;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Endpoints for managing struct (composite) data types in Ghidra.
|
||||
* Provides REST API for creating, listing, modifying, and deleting structs.
|
||||
*/
|
||||
public class StructEndpoints extends AbstractEndpoint {
|
||||
|
||||
private PluginTool tool;
|
||||
|
||||
public StructEndpoints(Program program, int port) {
|
||||
super(program, port);
|
||||
}
|
||||
|
||||
public StructEndpoints(Program program, int port, PluginTool tool) {
|
||||
super(program, port);
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginTool getTool() {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/structs", this::handleStructs);
|
||||
server.createContext("/structs/create", exchange -> {
|
||||
try {
|
||||
if ("POST".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
handleCreateStruct(exchange, params);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /structs/create endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
server.createContext("/structs/delete", exchange -> {
|
||||
try {
|
||||
if ("POST".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
handleDeleteStruct(exchange, params);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /structs/delete endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
server.createContext("/structs/addfield", exchange -> {
|
||||
try {
|
||||
if ("POST".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
handleAddField(exchange, params);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /structs/addfield endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
server.createContext("/structs/updatefield", exchange -> {
|
||||
try {
|
||||
if ("POST".equals(exchange.getRequestMethod()) || "PATCH".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
handleUpdateField(exchange, params);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /structs/updatefield endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /structs - list all structs, or GET /structs?name=X - get specific struct details
|
||||
*/
|
||||
private void handleStructs(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
String structName = qparams.get("name");
|
||||
|
||||
if (structName != null && !structName.isEmpty()) {
|
||||
handleGetStruct(exchange, structName);
|
||||
} else {
|
||||
handleListStructs(exchange);
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /structs endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all struct data types in the program
|
||||
*/
|
||||
private void handleListStructs(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
String categoryFilter = qparams.get("category");
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
List<Map<String, Object>> structList = new ArrayList<>();
|
||||
|
||||
// Iterate through all data types and filter for structures
|
||||
dtm.getAllDataTypes().forEachRemaining(dataType -> {
|
||||
if (dataType instanceof Structure) {
|
||||
Structure struct = (Structure) dataType;
|
||||
|
||||
// Apply category filter if specified
|
||||
if (categoryFilter != null && !categoryFilter.isEmpty()) {
|
||||
CategoryPath catPath = struct.getCategoryPath();
|
||||
if (!catPath.getPath().contains(categoryFilter)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> structInfo = new HashMap<>();
|
||||
structInfo.put("name", struct.getName());
|
||||
structInfo.put("path", struct.getPathName());
|
||||
structInfo.put("size", struct.getLength());
|
||||
structInfo.put("numFields", struct.getNumComponents());
|
||||
structInfo.put("category", struct.getCategoryPath().getPath());
|
||||
structInfo.put("description", struct.getDescription() != null ? struct.getDescription() : "");
|
||||
|
||||
// Add HATEOAS links
|
||||
Map<String, Object> links = new HashMap<>();
|
||||
Map<String, String> selfLink = new HashMap<>();
|
||||
selfLink.put("href", "/structs?name=" + struct.getName());
|
||||
links.put("self", selfLink);
|
||||
structInfo.put("_links", links);
|
||||
|
||||
structList.add(structInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by name for consistency
|
||||
structList.sort(Comparator.comparing(s -> (String) s.get("name")));
|
||||
|
||||
// Build response with pagination
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port).success(true);
|
||||
List<Map<String, Object>> paginated = applyPagination(structList, offset, limit, builder, "/structs");
|
||||
builder.result(paginated);
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error listing structs", e);
|
||||
sendErrorResponse(exchange, 500, "Error listing structs: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific struct including all fields
|
||||
*/
|
||||
private void handleGetStruct(HttpExchange exchange, String structName) throws IOException {
|
||||
try {
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// Try to find the struct - support both full paths and simple names
|
||||
DataType dataType = null;
|
||||
|
||||
// If it looks like a full path (starts with /), try direct lookup
|
||||
if (structName.startsWith("/")) {
|
||||
dataType = dtm.getDataType(structName);
|
||||
if (dataType == null) {
|
||||
dataType = dtm.findDataType(structName);
|
||||
}
|
||||
} else {
|
||||
// Search by simple name using the helper method
|
||||
dataType = findStructByName(dtm, structName);
|
||||
}
|
||||
|
||||
if (dataType == null || !(dataType instanceof Structure)) {
|
||||
sendErrorResponse(exchange, 404, "Struct not found: " + structName, "STRUCT_NOT_FOUND");
|
||||
return;
|
||||
}
|
||||
|
||||
Structure struct = (Structure) dataType;
|
||||
Map<String, Object> structInfo = buildStructInfo(struct);
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(structInfo);
|
||||
|
||||
builder.addLink("self", "/structs?name=" + struct.getName());
|
||||
builder.addLink("structs", "/structs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error getting struct details", e);
|
||||
sendErrorResponse(exchange, 500, "Error getting struct: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new struct data type
|
||||
* POST /structs/create
|
||||
* Required params: name
|
||||
* Optional params: category, size, description
|
||||
*/
|
||||
private void handleCreateStruct(HttpExchange exchange, Map<String, String> params) throws IOException {
|
||||
try {
|
||||
String structName = params.get("name");
|
||||
String category = params.get("category");
|
||||
String sizeStr = params.get("size");
|
||||
String description = params.get("description");
|
||||
|
||||
if (structName == null || structName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: name", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("name", structName);
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Create Struct", () -> {
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// Check if struct already exists
|
||||
DataType existing = dtm.getDataType("/" + structName);
|
||||
if (existing != null) {
|
||||
throw new Exception("Struct already exists: " + structName);
|
||||
}
|
||||
|
||||
// Determine category path
|
||||
CategoryPath catPath;
|
||||
if (category != null && !category.isEmpty()) {
|
||||
catPath = new CategoryPath(category);
|
||||
} else {
|
||||
catPath = CategoryPath.ROOT;
|
||||
}
|
||||
|
||||
// Create the structure
|
||||
StructureDataType struct = new StructureDataType(catPath, structName, 0);
|
||||
|
||||
if (description != null && !description.isEmpty()) {
|
||||
struct.setDescription(description);
|
||||
}
|
||||
|
||||
// Add to data type manager
|
||||
Structure addedStruct = (Structure) dtm.addDataType(struct, DataTypeConflictHandler.DEFAULT_HANDLER);
|
||||
|
||||
resultMap.put("path", addedStruct.getPathName());
|
||||
resultMap.put("category", addedStruct.getCategoryPath().getPath());
|
||||
resultMap.put("size", addedStruct.getLength());
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
resultMap.put("message", "Struct created successfully");
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(resultMap);
|
||||
|
||||
builder.addLink("self", "/structs?name=" + structName);
|
||||
builder.addLink("structs", "/structs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 201);
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Create Struct", e);
|
||||
sendErrorResponse(exchange, 500, "Failed to create struct: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error creating struct", e);
|
||||
sendErrorResponse(exchange, 400, "Error creating struct: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Unexpected error creating struct", e);
|
||||
sendErrorResponse(exchange, 500, "Error creating struct: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field to an existing struct
|
||||
* POST /structs/addfield
|
||||
* Required params: struct, fieldName, fieldType
|
||||
* Optional params: offset, comment
|
||||
*/
|
||||
private void handleAddField(HttpExchange exchange, Map<String, String> params) throws IOException {
|
||||
try {
|
||||
String structName = params.get("struct");
|
||||
String fieldName = params.get("fieldName");
|
||||
String fieldType = params.get("fieldType");
|
||||
String offsetStr = params.get("offset");
|
||||
String comment = params.get("comment");
|
||||
|
||||
if (structName == null || structName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: struct", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
if (fieldName == null || fieldName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: fieldName", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
if (fieldType == null || fieldType.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: fieldType", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
Integer offset = null;
|
||||
if (offsetStr != null && !offsetStr.isEmpty()) {
|
||||
try {
|
||||
offset = Integer.parseInt(offsetStr);
|
||||
} catch (NumberFormatException e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid offset parameter: must be an integer", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("struct", structName);
|
||||
resultMap.put("fieldName", fieldName);
|
||||
resultMap.put("fieldType", fieldType);
|
||||
|
||||
final Integer finalOffset = offset;
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Add Struct Field", () -> {
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// Find the struct - handle both full paths and simple names
|
||||
DataType dataType = null;
|
||||
if (structName.startsWith("/")) {
|
||||
dataType = dtm.getDataType(structName);
|
||||
if (dataType == null) {
|
||||
dataType = dtm.findDataType(structName);
|
||||
}
|
||||
} else {
|
||||
dataType = findStructByName(dtm, structName);
|
||||
}
|
||||
|
||||
if (dataType == null || !(dataType instanceof Structure)) {
|
||||
throw new Exception("Struct not found: " + structName);
|
||||
}
|
||||
|
||||
Structure struct = (Structure) dataType;
|
||||
|
||||
// Find the field type
|
||||
DataType fieldDataType = findDataType(dtm, fieldType);
|
||||
if (fieldDataType == null) {
|
||||
throw new Exception("Field type not found: " + fieldType);
|
||||
}
|
||||
|
||||
// Add the field
|
||||
DataTypeComponent component;
|
||||
if (finalOffset != null) {
|
||||
// Insert at specific offset
|
||||
component = struct.insertAtOffset(finalOffset, fieldDataType,
|
||||
fieldDataType.getLength(), fieldName, comment);
|
||||
} else {
|
||||
// Append to end
|
||||
component = struct.add(fieldDataType, fieldName, comment);
|
||||
}
|
||||
|
||||
resultMap.put("offset", component.getOffset());
|
||||
resultMap.put("length", component.getLength());
|
||||
resultMap.put("structSize", struct.getLength());
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
resultMap.put("message", "Field added successfully");
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(resultMap);
|
||||
|
||||
builder.addLink("struct", "/structs?name=" + structName);
|
||||
builder.addLink("structs", "/structs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Add Struct Field", e);
|
||||
sendErrorResponse(exchange, 500, "Failed to add field: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error adding field", e);
|
||||
sendErrorResponse(exchange, 400, "Error adding field: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Unexpected error adding field", e);
|
||||
sendErrorResponse(exchange, 500, "Error adding field: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing field in a struct
|
||||
* POST/PATCH /structs/updatefield
|
||||
* Required params: struct, fieldOffset (or fieldName)
|
||||
* Optional params: newName, newType, newComment
|
||||
*/
|
||||
private void handleUpdateField(HttpExchange exchange, Map<String, String> params) throws IOException {
|
||||
try {
|
||||
String structName = params.get("struct");
|
||||
String fieldOffsetStr = params.get("fieldOffset");
|
||||
String fieldName = params.get("fieldName");
|
||||
String newName = params.get("newName");
|
||||
String newType = params.get("newType");
|
||||
String newComment = params.get("newComment");
|
||||
|
||||
if (structName == null || structName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: struct", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Must have either fieldOffset or fieldName to identify the field
|
||||
if ((fieldOffsetStr == null || fieldOffsetStr.isEmpty()) && (fieldName == null || fieldName.isEmpty())) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: either fieldOffset or fieldName must be provided", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Must have at least one update parameter
|
||||
if ((newName == null || newName.isEmpty()) &&
|
||||
(newType == null || newType.isEmpty()) &&
|
||||
(newComment == null || newComment.isEmpty())) {
|
||||
sendErrorResponse(exchange, 400, "At least one of newName, newType, or newComment must be provided", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
Integer fieldOffset = null;
|
||||
if (fieldOffsetStr != null && !fieldOffsetStr.isEmpty()) {
|
||||
try {
|
||||
fieldOffset = Integer.parseInt(fieldOffsetStr);
|
||||
} catch (NumberFormatException e) {
|
||||
sendErrorResponse(exchange, 400, "Invalid fieldOffset parameter: must be an integer", "INVALID_PARAMETER");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("struct", structName);
|
||||
|
||||
final Integer finalFieldOffset = fieldOffset;
|
||||
final String finalFieldName = fieldName;
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Update Struct Field", () -> {
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// Find the struct
|
||||
DataType dataType = null;
|
||||
if (structName.startsWith("/")) {
|
||||
dataType = dtm.getDataType(structName);
|
||||
if (dataType == null) {
|
||||
dataType = dtm.findDataType(structName);
|
||||
}
|
||||
} else {
|
||||
dataType = findStructByName(dtm, structName);
|
||||
}
|
||||
|
||||
if (dataType == null || !(dataType instanceof Structure)) {
|
||||
throw new Exception("Struct not found: " + structName);
|
||||
}
|
||||
|
||||
Structure struct = (Structure) dataType;
|
||||
|
||||
// Find the field to update
|
||||
DataTypeComponent component = null;
|
||||
if (finalFieldOffset != null) {
|
||||
component = struct.getComponentAt(finalFieldOffset);
|
||||
} else {
|
||||
// Search by field name
|
||||
for (DataTypeComponent comp : struct.getComponents()) {
|
||||
if (finalFieldName.equals(comp.getFieldName())) {
|
||||
component = comp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (component == null) {
|
||||
throw new Exception("Field not found in struct: " + (finalFieldOffset != null ? "offset " + finalFieldOffset : finalFieldName));
|
||||
}
|
||||
|
||||
int componentOffset = component.getOffset();
|
||||
int componentLength = component.getLength();
|
||||
DataType originalType = component.getDataType();
|
||||
String originalName = component.getFieldName();
|
||||
String originalComment = component.getComment();
|
||||
|
||||
// Store original values
|
||||
resultMap.put("originalName", originalName);
|
||||
resultMap.put("originalType", originalType.getName());
|
||||
resultMap.put("originalComment", originalComment != null ? originalComment : "");
|
||||
resultMap.put("offset", componentOffset);
|
||||
|
||||
// Determine new values
|
||||
String updatedName = (newName != null && !newName.isEmpty()) ? newName : originalName;
|
||||
String updatedComment = (newComment != null) ? newComment : originalComment;
|
||||
DataType updatedType = originalType;
|
||||
|
||||
if (newType != null && !newType.isEmpty()) {
|
||||
updatedType = findDataType(dtm, newType);
|
||||
if (updatedType == null) {
|
||||
throw new Exception("Field type not found: " + newType);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the field by replacing it
|
||||
// Ghidra doesn't have a direct "update" - we need to delete and re-add
|
||||
struct.deleteAtOffset(componentOffset);
|
||||
DataTypeComponent newComponent = struct.insertAtOffset(componentOffset, updatedType,
|
||||
updatedType.getLength(),
|
||||
updatedName, updatedComment);
|
||||
|
||||
resultMap.put("newName", newComponent.getFieldName());
|
||||
resultMap.put("newType", newComponent.getDataType().getName());
|
||||
resultMap.put("newComment", newComponent.getComment() != null ? newComponent.getComment() : "");
|
||||
resultMap.put("length", newComponent.getLength());
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
resultMap.put("message", "Field updated successfully");
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(resultMap);
|
||||
|
||||
builder.addLink("struct", "/structs?name=" + structName);
|
||||
builder.addLink("structs", "/structs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Update Struct Field", e);
|
||||
sendErrorResponse(exchange, 500, "Failed to update field: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error updating field", e);
|
||||
sendErrorResponse(exchange, 400, "Error updating field: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Unexpected error updating field", e);
|
||||
sendErrorResponse(exchange, 500, "Error updating field: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a struct data type
|
||||
* POST /structs/delete
|
||||
* Required params: name
|
||||
*/
|
||||
private void handleDeleteStruct(HttpExchange exchange, Map<String, String> params) throws IOException {
|
||||
try {
|
||||
String structName = params.get("name");
|
||||
|
||||
if (structName == null || structName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameter: name", "MISSING_PARAMETERS");
|
||||
return;
|
||||
}
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("name", structName);
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(program, "Delete Struct", () -> {
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// Find the struct - handle both full paths and simple names
|
||||
DataType dataType = null;
|
||||
if (structName.startsWith("/")) {
|
||||
dataType = dtm.getDataType(structName);
|
||||
if (dataType == null) {
|
||||
dataType = dtm.findDataType(structName);
|
||||
}
|
||||
} else {
|
||||
dataType = findStructByName(dtm, structName);
|
||||
}
|
||||
|
||||
if (dataType == null) {
|
||||
throw new Exception("Struct not found: " + structName);
|
||||
}
|
||||
|
||||
if (!(dataType instanceof Structure)) {
|
||||
throw new Exception("Data type is not a struct: " + structName);
|
||||
}
|
||||
|
||||
// Store info before deletion
|
||||
resultMap.put("path", dataType.getPathName());
|
||||
resultMap.put("category", dataType.getCategoryPath().getPath());
|
||||
|
||||
// Remove the struct
|
||||
dtm.remove(dataType, null);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
resultMap.put("message", "Struct deleted successfully");
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(resultMap);
|
||||
|
||||
builder.addLink("structs", "/structs");
|
||||
builder.addLink("program", "/program");
|
||||
|
||||
sendJsonResponse(exchange, builder.build(), 200);
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Delete Struct", e);
|
||||
sendErrorResponse(exchange, 500, "Failed to delete struct: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error deleting struct", e);
|
||||
sendErrorResponse(exchange, 400, "Error deleting struct: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Unexpected error deleting struct", e);
|
||||
sendErrorResponse(exchange, 500, "Error deleting struct: " + e.getMessage(), "INTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a detailed information map for a struct including all fields
|
||||
*/
|
||||
private Map<String, Object> buildStructInfo(Structure struct) {
|
||||
Map<String, Object> structInfo = new HashMap<>();
|
||||
structInfo.put("name", struct.getName());
|
||||
structInfo.put("path", struct.getPathName());
|
||||
structInfo.put("size", struct.getLength());
|
||||
structInfo.put("category", struct.getCategoryPath().getPath());
|
||||
structInfo.put("description", struct.getDescription() != null ? struct.getDescription() : "");
|
||||
structInfo.put("numFields", struct.getNumComponents());
|
||||
|
||||
// Add field details
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
for (DataTypeComponent component : struct.getComponents()) {
|
||||
Map<String, Object> fieldInfo = new HashMap<>();
|
||||
fieldInfo.put("name", component.getFieldName() != null ? component.getFieldName() : "");
|
||||
fieldInfo.put("offset", component.getOffset());
|
||||
fieldInfo.put("length", component.getLength());
|
||||
fieldInfo.put("type", component.getDataType().getName());
|
||||
fieldInfo.put("typePath", component.getDataType().getPathName());
|
||||
fieldInfo.put("comment", component.getComment() != null ? component.getComment() : "");
|
||||
fields.add(fieldInfo);
|
||||
}
|
||||
structInfo.put("fields", fields);
|
||||
|
||||
return structInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a struct by name, searching through all data types
|
||||
*/
|
||||
private DataType findStructByName(DataTypeManager dtm, String structName) {
|
||||
final DataType[] result = new DataType[1];
|
||||
|
||||
dtm.getAllDataTypes().forEachRemaining(dt -> {
|
||||
if (dt instanceof Structure && dt.getName().equals(structName)) {
|
||||
if (result[0] == null) {
|
||||
result[0] = dt;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a data type by name, trying multiple lookup methods
|
||||
*/
|
||||
private DataType findDataType(DataTypeManager dtm, String typeName) {
|
||||
// Try direct lookup with path
|
||||
DataType dataType = dtm.getDataType("/" + typeName);
|
||||
|
||||
// Try without path
|
||||
if (dataType == null) {
|
||||
dataType = dtm.findDataType("/" + typeName);
|
||||
}
|
||||
|
||||
// Try built-in primitive types
|
||||
if (dataType == null) {
|
||||
switch(typeName.toLowerCase()) {
|
||||
case "byte":
|
||||
dataType = new ByteDataType();
|
||||
break;
|
||||
case "char":
|
||||
dataType = new CharDataType();
|
||||
break;
|
||||
case "word":
|
||||
dataType = new WordDataType();
|
||||
break;
|
||||
case "dword":
|
||||
dataType = new DWordDataType();
|
||||
break;
|
||||
case "qword":
|
||||
dataType = new QWordDataType();
|
||||
break;
|
||||
case "float":
|
||||
dataType = new FloatDataType();
|
||||
break;
|
||||
case "double":
|
||||
dataType = new DoubleDataType();
|
||||
break;
|
||||
case "int":
|
||||
dataType = new IntegerDataType();
|
||||
break;
|
||||
case "long":
|
||||
dataType = new LongDataType();
|
||||
break;
|
||||
case "pointer":
|
||||
dataType = new PointerDataType();
|
||||
break;
|
||||
case "string":
|
||||
dataType = new StringDataType();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dataType;
|
||||
}
|
||||
}
|
||||
@ -58,9 +58,6 @@ package eu.starsong.ghidra.endpoints;
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/variables", this::handleGlobalVariables);
|
||||
// Note: /functions/{name}/variables is handled within FunctionEndpoints for now
|
||||
// to keep related logic together until full refactor.
|
||||
// If needed, we can create a more complex routing mechanism later.
|
||||
}
|
||||
|
||||
private void handleGlobalVariables(HttpExchange exchange) throws IOException {
|
||||
|
||||
@ -364,7 +364,7 @@ public class GhidraUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about variables in a function.
|
||||
* Gets information about variables in a function, including decompiler variables.
|
||||
* @param function The function to get variables from.
|
||||
* @return A list of maps containing information about each variable.
|
||||
*/
|
||||
@ -381,10 +381,12 @@ public class GhidraUtil {
|
||||
varInfo.put("name", param.getName());
|
||||
varInfo.put("type", param.getDataType().getName());
|
||||
varInfo.put("isParameter", true);
|
||||
varInfo.put("storage", param.getVariableStorage().toString());
|
||||
varInfo.put("source", "database");
|
||||
variables.add(varInfo);
|
||||
}
|
||||
|
||||
// Add local variables
|
||||
// Add local variables from database
|
||||
for (Variable var : function.getAllVariables()) {
|
||||
if (var instanceof Parameter) {
|
||||
continue; // Skip parameters, already added
|
||||
@ -394,9 +396,63 @@ public class GhidraUtil {
|
||||
varInfo.put("name", var.getName());
|
||||
varInfo.put("type", var.getDataType().getName());
|
||||
varInfo.put("isParameter", false);
|
||||
varInfo.put("storage", var.getVariableStorage().toString());
|
||||
varInfo.put("source", "database");
|
||||
variables.add(varInfo);
|
||||
}
|
||||
|
||||
// Add decompiler-generated variables
|
||||
DecompInterface decompiler = new DecompInterface();
|
||||
try {
|
||||
decompiler.openProgram(function.getProgram());
|
||||
DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
|
||||
|
||||
if (results.decompileCompleted()) {
|
||||
HighFunction highFunc = results.getHighFunction();
|
||||
if (highFunc != null) {
|
||||
// Iterate over local variables from decompiler
|
||||
for (java.util.Iterator<ghidra.program.model.pcode.HighSymbol> iter =
|
||||
highFunc.getLocalSymbolMap().getSymbols(); iter.hasNext(); ) {
|
||||
|
||||
ghidra.program.model.pcode.HighSymbol highSymbol = iter.next();
|
||||
|
||||
// Skip if this is already a tracked variable
|
||||
boolean alreadyAdded = false;
|
||||
for (Map<String, Object> var : variables) {
|
||||
if (var.get("name").equals(highSymbol.getName())) {
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
Map<String, Object> varInfo = new HashMap<>();
|
||||
varInfo.put("name", highSymbol.getName());
|
||||
varInfo.put("type", highSymbol.getDataType() != null ?
|
||||
highSymbol.getDataType().getName() : "unknown");
|
||||
varInfo.put("isParameter", highSymbol.isParameter());
|
||||
varInfo.put("storage", highSymbol.getStorage() != null ?
|
||||
highSymbol.getStorage().toString() : "unknown");
|
||||
varInfo.put("source", "decompiler");
|
||||
|
||||
// Add PC address if available
|
||||
if (highSymbol.getPCAddress() != null) {
|
||||
varInfo.put("pcAddress", highSymbol.getPCAddress().toString());
|
||||
}
|
||||
|
||||
variables.add(varInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(GhidraUtil.class, "Error analyzing decompiler variables", e);
|
||||
}
|
||||
finally {
|
||||
decompiler.dispose();
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,10 @@ public class TransactionHelper {
|
||||
Msg.error(TransactionHelper.class, "Transaction failed: " + transactionName, e);
|
||||
} finally {
|
||||
if (txId >= 0) {
|
||||
program.endTransaction(txId, success);
|
||||
if (!program.endTransaction(txId, success)) {
|
||||
Msg.error(TransactionHelper.class, "Failed to end transaction: " + transactionName);
|
||||
exception.set(new TransactionException("Failed to end transaction: " + transactionName));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
Manifest-Version: 1.0
|
||||
Plugin-Class: eu.starsong.ghidra.GhydraMCP
|
||||
Plugin-Name: GhydraMCP
|
||||
Plugin-Version: 11.3.1
|
||||
Plugin-Class: eu.starsong.ghidra.MCGhidra
|
||||
Plugin-Name: MCGhidra
|
||||
Plugin-Version: 11.4.2
|
||||
Bundle-Version: dev-SNAPSHOT
|
||||
Plugin-Author: LaurieWired, Teal Bauer
|
||||
Plugin-Description: Expose multiple Ghidra tools to MCP servers with variable management
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
Manifest-Version: 1.0
|
||||
GHIDRA_MODULE_NAME: GhydraMCP
|
||||
GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||
# MCGhidra Module Manifest
|
||||
#
|
||||
# This file lists third-party libraries bundled with this extension and their licenses.
|
||||
# Module metadata (name, description, version) is defined in extension.properties.
|
||||
#
|
||||
# Format: MODULE FILE LICENSE: lib/filename.jar License Name
|
||||
#
|
||||
# Currently, MCGhidra has no bundled third-party libraries.
|
||||
# Gson is provided by Ghidra itself.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
name=GhydraMCP
|
||||
name=MCGhidra
|
||||
description=A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||
author=Laurie Wired, Teal Bauer
|
||||
createdOn=2025-03-29
|
||||
version=11.3.1
|
||||
ghidraVersion=11.3.1
|
||||
version=11.4.2
|
||||
ghidraVersion=11.4.2
|
||||
|
||||
15
src/mcghidra/__init__.py
Normal file
15
src/mcghidra/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""MCGhidra - Reverse engineering bridge for Ghidra.
|
||||
|
||||
Multi-instance Ghidra plugin with HATEOAS REST API and MCP server
|
||||
for decompilation, analysis & binary manipulation.
|
||||
"""
|
||||
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
__version__ = version("mcghidra")
|
||||
except Exception:
|
||||
__version__ = "2025.12.1"
|
||||
|
||||
from .server import create_server, main
|
||||
|
||||
__all__ = ["create_server", "main", "__version__"]
|
||||
9
src/mcghidra/__main__.py
Normal file
9
src/mcghidra/__main__.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""MCGhidra package entry point.
|
||||
|
||||
Allows running with: python -m mcghidra
|
||||
"""
|
||||
|
||||
from .server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
137
src/mcghidra/config.py
Normal file
137
src/mcghidra/config.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Configuration management for MCGhidra.
|
||||
|
||||
Handles environment variables, default settings, and runtime configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class DockerConfig:
|
||||
"""Docker-specific configuration."""
|
||||
|
||||
# Docker image settings
|
||||
image_name: str = "mcghidra"
|
||||
image_tag: str = field(default_factory=lambda: os.environ.get("MCGHIDRAMCP_VERSION", "latest"))
|
||||
|
||||
# Default container settings
|
||||
default_port: int = field(default_factory=lambda: int(os.environ.get("MCGHIDRA_PORT", "8192")))
|
||||
default_memory: str = field(default_factory=lambda: os.environ.get("MCGHIDRA_MAXMEM", "2G"))
|
||||
|
||||
# Project directory (for building)
|
||||
project_dir: Optional[Path] = None
|
||||
|
||||
# Auto-start settings
|
||||
auto_start_enabled: bool = field(default_factory=lambda: os.environ.get("MCGHIDRA_DOCKER_AUTO", "false").lower() == "true")
|
||||
auto_start_wait: bool = True
|
||||
auto_start_timeout: float = 300.0
|
||||
|
||||
|
||||
# Docker configuration instance
|
||||
_docker_config: Optional[DockerConfig] = None
|
||||
|
||||
|
||||
def get_docker_config() -> DockerConfig:
|
||||
"""Get the Docker configuration instance."""
|
||||
global _docker_config
|
||||
if _docker_config is None:
|
||||
_docker_config = DockerConfig()
|
||||
return _docker_config
|
||||
|
||||
|
||||
def set_docker_config(config: DockerConfig) -> None:
|
||||
"""Set the Docker configuration instance."""
|
||||
global _docker_config
|
||||
_docker_config = config
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCGhidraConfig:
|
||||
"""Configuration for MCGhidra server."""
|
||||
|
||||
# Ghidra connection settings
|
||||
ghidra_host: str = field(default_factory=lambda: os.environ.get("GHIDRA_HOST", "localhost"))
|
||||
default_port: Optional[int] = None
|
||||
|
||||
# Port scanning ranges for instance discovery
|
||||
quick_discovery_range: range = field(default_factory=lambda: range(18489, 18499))
|
||||
full_discovery_range: range = field(default_factory=lambda: range(18400, 18600))
|
||||
|
||||
# HTTP client settings
|
||||
request_timeout: float = 30.0
|
||||
discovery_timeout: float = 0.5
|
||||
|
||||
# Pagination defaults
|
||||
default_page_size: int = 50
|
||||
max_page_size: int = 500
|
||||
|
||||
# Cursor management
|
||||
cursor_ttl_seconds: int = 300 # 5 minutes
|
||||
max_cursors_per_session: int = 100
|
||||
|
||||
# Response size limits (for return_all guard)
|
||||
max_response_tokens: int = 8000 # Hard budget — guard triggers above this
|
||||
large_response_threshold: int = 4000 # Warn above this in normal pagination
|
||||
|
||||
# Expected API version
|
||||
expected_api_version: int = 2
|
||||
|
||||
# Feedback collection
|
||||
feedback_enabled: bool = field(
|
||||
default_factory=lambda: os.environ.get("MCGHIDRA_FEEDBACK", "true").lower() == "true"
|
||||
)
|
||||
feedback_db_path: str = field(
|
||||
default_factory=lambda: os.environ.get(
|
||||
"MCGHIDRA_FEEDBACK_DB",
|
||||
str(Path.home() / ".mcghidra" / "feedback.db"),
|
||||
)
|
||||
)
|
||||
|
||||
# Resource caps for enumeration endpoints
|
||||
resource_caps: dict = field(default_factory=lambda: {
|
||||
"functions": 1000,
|
||||
"strings": 500,
|
||||
"data": 1000,
|
||||
"structs": 500,
|
||||
"xrefs": 500,
|
||||
"symbols": 1000,
|
||||
"segments": 500,
|
||||
"variables": 1000,
|
||||
"namespaces": 500,
|
||||
"classes": 500,
|
||||
"bookmarks": 1000,
|
||||
"enums": 500,
|
||||
"typedefs": 500,
|
||||
})
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate configuration after initialization."""
|
||||
if self.default_page_size > self.max_page_size:
|
||||
self.default_page_size = self.max_page_size
|
||||
|
||||
|
||||
# Global configuration instance (can be replaced for testing)
|
||||
_config: Optional[MCGhidraConfig] = None
|
||||
|
||||
|
||||
def get_config() -> MCGhidraConfig:
|
||||
"""Get the global configuration instance."""
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = MCGhidraConfig()
|
||||
return _config
|
||||
|
||||
|
||||
def set_config(config: MCGhidraConfig) -> None:
|
||||
"""Set the global configuration instance."""
|
||||
global _config
|
||||
_config = config
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset to default configuration."""
|
||||
global _config
|
||||
_config = None
|
||||
67
src/mcghidra/core/__init__.py
Normal file
67
src/mcghidra/core/__init__.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Core infrastructure for MCGhidra.
|
||||
|
||||
Contains HTTP client, pagination, progress reporting, and logging utilities.
|
||||
"""
|
||||
|
||||
from .filtering import (
|
||||
apply_grep,
|
||||
estimate_and_guard,
|
||||
project_fields,
|
||||
)
|
||||
from .http_client import (
|
||||
get_instance_url,
|
||||
safe_delete,
|
||||
safe_get,
|
||||
safe_patch,
|
||||
safe_post,
|
||||
safe_put,
|
||||
simplify_response,
|
||||
)
|
||||
from .logging import (
|
||||
log_debug,
|
||||
log_error,
|
||||
log_info,
|
||||
log_warning,
|
||||
)
|
||||
from .pagination import (
|
||||
CursorManager,
|
||||
CursorState,
|
||||
estimate_tokens,
|
||||
get_cursor_manager,
|
||||
paginate_response,
|
||||
)
|
||||
from .progress import (
|
||||
ProgressReporter,
|
||||
report_progress,
|
||||
report_step,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# HTTP client
|
||||
"safe_get",
|
||||
"safe_post",
|
||||
"safe_put",
|
||||
"safe_patch",
|
||||
"safe_delete",
|
||||
"simplify_response",
|
||||
"get_instance_url",
|
||||
# Pagination
|
||||
"CursorManager",
|
||||
"CursorState",
|
||||
"paginate_response",
|
||||
"get_cursor_manager",
|
||||
"estimate_tokens",
|
||||
# Progress
|
||||
"ProgressReporter",
|
||||
"report_progress",
|
||||
"report_step",
|
||||
# Filtering
|
||||
"project_fields",
|
||||
"apply_grep",
|
||||
"estimate_and_guard",
|
||||
# Logging
|
||||
"log_info",
|
||||
"log_debug",
|
||||
"log_warning",
|
||||
"log_error",
|
||||
]
|
||||
237
src/mcghidra/core/filtering.py
Normal file
237
src/mcghidra/core/filtering.py
Normal file
@ -0,0 +1,237 @@
|
||||
"""Field projection and response size guard for MCGhidra.
|
||||
|
||||
Provides jq-style field projection, grep filtering, and token budget
|
||||
enforcement to prevent oversized MCP tool results.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..config import get_config
|
||||
|
||||
# Token estimation (same ratio as pagination.py)
|
||||
TOKEN_ESTIMATION_RATIO = 4.0
|
||||
|
||||
|
||||
def project_fields(items: List[Any], fields: List[str]) -> List[Any]:
|
||||
"""Select only specified keys from each item (jq-style projection).
|
||||
|
||||
Works on dicts and strings. For dicts, returns only the requested
|
||||
keys. For non-dict items (e.g. lines of decompiled code), returns
|
||||
them unchanged.
|
||||
|
||||
Args:
|
||||
items: List of items to project
|
||||
fields: List of field names to keep
|
||||
|
||||
Returns:
|
||||
List of projected items
|
||||
"""
|
||||
if not fields or not items:
|
||||
return items
|
||||
|
||||
field_set = set(fields)
|
||||
projected = []
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
projected.append({k: v for k, v in item.items() if k in field_set})
|
||||
else:
|
||||
projected.append(item)
|
||||
return projected
|
||||
|
||||
|
||||
def apply_grep(items: List[Any], pattern: str, ignorecase: bool = True) -> List[Any]:
|
||||
"""Filter items by regex pattern across all string values.
|
||||
|
||||
Searches all string-coercible values in each item. For dicts,
|
||||
searches all values recursively. For strings, searches directly.
|
||||
|
||||
Args:
|
||||
items: List of items to filter
|
||||
pattern: Regex pattern string
|
||||
ignorecase: Case-insensitive matching (default True)
|
||||
|
||||
Returns:
|
||||
Filtered list of matching items
|
||||
"""
|
||||
if not pattern or not items:
|
||||
return items
|
||||
|
||||
flags = re.IGNORECASE if ignorecase else 0
|
||||
compiled = re.compile(pattern, flags)
|
||||
|
||||
return [item for item in items if _matches(item, compiled)]
|
||||
|
||||
|
||||
def _matches(item: Any, pattern: re.Pattern, depth: int = 0) -> bool:
|
||||
"""Check if item matches pattern (recursive for nested structures)."""
|
||||
if depth > 10:
|
||||
return False
|
||||
|
||||
if isinstance(item, dict):
|
||||
for value in item.values():
|
||||
if isinstance(value, str) and pattern.search(value):
|
||||
return True
|
||||
elif isinstance(value, (int, float)):
|
||||
if pattern.search(str(value)):
|
||||
return True
|
||||
elif isinstance(value, (dict, list, tuple)):
|
||||
if _matches(value, pattern, depth + 1):
|
||||
return True
|
||||
return False
|
||||
elif isinstance(item, (list, tuple)):
|
||||
return any(_matches(i, pattern, depth + 1) for i in item)
|
||||
elif isinstance(item, str):
|
||||
return bool(pattern.search(item))
|
||||
else:
|
||||
return bool(pattern.search(str(item)))
|
||||
|
||||
|
||||
def _estimate_tokens(data: Any) -> int:
|
||||
"""Estimate token count from serialized JSON size.
|
||||
|
||||
Uses a simple heuristic: ~4 characters per token on average.
|
||||
This matches the TOKEN_ESTIMATION_RATIO constant.
|
||||
|
||||
Args:
|
||||
data: Any JSON-serializable data structure
|
||||
|
||||
Returns:
|
||||
Estimated token count
|
||||
"""
|
||||
text = json.dumps(data, default=str)
|
||||
return int(len(text) / TOKEN_ESTIMATION_RATIO)
|
||||
|
||||
|
||||
def _extract_available_fields(items: List[Any]) -> List[str]:
|
||||
"""Extract the set of field names from the first few dict items.
|
||||
|
||||
Samples up to 5 items to discover available keys, useful for
|
||||
suggesting field projections to reduce response size.
|
||||
|
||||
Args:
|
||||
items: List of items (only dicts are examined)
|
||||
|
||||
Returns:
|
||||
Sorted list of unique field names (excludes internal _links)
|
||||
"""
|
||||
fields = set()
|
||||
for item in items[:5]:
|
||||
if isinstance(item, dict):
|
||||
fields.update(item.keys())
|
||||
# Remove internal/HATEOAS fields
|
||||
fields.discard("_links")
|
||||
return sorted(fields)
|
||||
|
||||
|
||||
def estimate_and_guard(
|
||||
data: List[Any],
|
||||
tool_name: str,
|
||||
budget: Optional[int] = None,
|
||||
query_hints: Optional[Dict[str, Any]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Check if data exceeds token budget; return guard response if so.
|
||||
|
||||
If data fits within budget, returns None (caller should proceed
|
||||
normally). If data exceeds budget, returns a structured summary
|
||||
with instructions for narrowing the query.
|
||||
|
||||
Args:
|
||||
data: The full data list to check
|
||||
tool_name: Name of the tool (for hint messages)
|
||||
budget: Token budget override (defaults to config.max_response_tokens)
|
||||
query_hints: Original query params (for building hint commands)
|
||||
|
||||
Returns:
|
||||
None if data fits within budget, or a guard response dict
|
||||
"""
|
||||
config = get_config()
|
||||
if budget is None:
|
||||
budget = config.max_response_tokens
|
||||
|
||||
estimated = _estimate_tokens(data)
|
||||
if estimated <= budget:
|
||||
return None
|
||||
|
||||
# Build sample from first 3 items
|
||||
sample = data[:3]
|
||||
available_fields = _extract_available_fields(data)
|
||||
|
||||
# Build actionable hints based on the tool name
|
||||
hints = _build_hints(tool_name, available_fields, query_hints)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"guarded": True,
|
||||
"total_count": len(data),
|
||||
"estimated_tokens": estimated,
|
||||
"budget": budget,
|
||||
"sample": sample,
|
||||
"available_fields": available_fields,
|
||||
"message": (
|
||||
"Response too large (%d items, ~%s tokens, budget: %s). "
|
||||
"To read this data:\n%s"
|
||||
) % (
|
||||
len(data),
|
||||
_format_tokens(estimated),
|
||||
_format_tokens(budget),
|
||||
hints,
|
||||
),
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
|
||||
def _format_tokens(n: int) -> str:
|
||||
"""Format token count for human-readable display.
|
||||
|
||||
Large numbers are abbreviated with 'k' suffix for readability
|
||||
in error messages and hints.
|
||||
|
||||
Args:
|
||||
n: Token count
|
||||
|
||||
Returns:
|
||||
Formatted string (e.g., 45000 -> '45k', 500 -> '500')
|
||||
"""
|
||||
if n >= 1000:
|
||||
return "%dk" % (n // 1000)
|
||||
return str(n)
|
||||
|
||||
|
||||
def _build_hints(
|
||||
tool_name: str,
|
||||
available_fields: List[str],
|
||||
query_hints: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""Build actionable hint text for the guard message."""
|
||||
lines = []
|
||||
|
||||
# Pagination hint
|
||||
lines.append(
|
||||
" - Paginate: %s(page_size=50) then cursor_next(cursor_id='...')"
|
||||
% tool_name
|
||||
)
|
||||
|
||||
# Grep hint
|
||||
grep_example = "main" if "functions" in tool_name else ".*pattern.*"
|
||||
lines.append(
|
||||
" - Filter: %s(grep='%s')" % (tool_name, grep_example)
|
||||
)
|
||||
|
||||
# Fields hint (only if we have dict items with fields)
|
||||
if available_fields:
|
||||
short_fields = available_fields[:2]
|
||||
lines.append(
|
||||
" - Project: %s(fields=%s)" % (tool_name, short_fields)
|
||||
)
|
||||
|
||||
# Combined hint
|
||||
if available_fields:
|
||||
lines.append(
|
||||
" - Combine: %s(grep='...', fields=%s, return_all=True)"
|
||||
% (tool_name, available_fields[:2])
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
400
src/mcghidra/core/http_client.py
Normal file
400
src/mcghidra/core/http_client.py
Normal file
@ -0,0 +1,400 @@
|
||||
"""HTTP client for Ghidra REST API communication.
|
||||
|
||||
Provides safe request methods with error handling, HATEOAS compliance,
|
||||
and response simplification for MCP tool consumption.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from ..config import get_config
|
||||
|
||||
# Allowed origins for CORS-like validation
|
||||
ALLOWED_ORIGINS = {
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"https://localhost",
|
||||
"https://127.0.0.1",
|
||||
}
|
||||
|
||||
|
||||
def validate_origin(headers: Dict[str, str]) -> bool:
|
||||
"""Validate request origin against allowed origins.
|
||||
|
||||
Args:
|
||||
headers: Request headers dict
|
||||
|
||||
Returns:
|
||||
True if origin is allowed or not present
|
||||
"""
|
||||
origin = headers.get("Origin")
|
||||
if not origin:
|
||||
# No origin header - allow (browser same-origin policy applies)
|
||||
return True
|
||||
|
||||
try:
|
||||
parsed = urlparse(origin)
|
||||
origin_base = f"{parsed.scheme}://{parsed.hostname}"
|
||||
if parsed.port:
|
||||
origin_base += f":{parsed.port}"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return origin_base in ALLOWED_ORIGINS
|
||||
|
||||
|
||||
def get_instance_url(port: int, host: Optional[str] = None) -> str:
|
||||
"""Get URL for a Ghidra instance by port.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
host: Optional host override (defaults to config)
|
||||
|
||||
Returns:
|
||||
Full URL for the Ghidra instance
|
||||
"""
|
||||
if host is None:
|
||||
host = get_config().ghidra_host
|
||||
return f"http://{host}:{port}"
|
||||
|
||||
|
||||
def _make_request(
|
||||
method: str,
|
||||
port: int,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[str] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
host: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Make HTTP request to Ghidra instance with error handling.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
params: Query parameters
|
||||
json_data: JSON payload for POST/PUT/PATCH
|
||||
data: Raw text payload
|
||||
headers: Additional headers
|
||||
host: Optional host override
|
||||
|
||||
Returns:
|
||||
Response dict with success flag and result or error
|
||||
"""
|
||||
config = get_config()
|
||||
url = f"{get_instance_url(port, host)}/{endpoint}"
|
||||
|
||||
# Set up headers for HATEOAS API
|
||||
request_headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Request-ID": f"mcp-bridge-{int(time.time() * 1000)}",
|
||||
}
|
||||
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
|
||||
# Validate origin for state-changing requests
|
||||
is_state_changing = method.upper() in ["POST", "PUT", "PATCH", "DELETE"]
|
||||
if is_state_changing:
|
||||
check_headers = (
|
||||
json_data.get("headers", {})
|
||||
if isinstance(json_data, dict)
|
||||
else (headers or {})
|
||||
)
|
||||
if not validate_origin(check_headers):
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "ORIGIN_NOT_ALLOWED",
|
||||
"message": "Origin not allowed for state-changing request",
|
||||
},
|
||||
"status_code": 403,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
if json_data is not None:
|
||||
request_headers["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
request_headers["Content-Type"] = "text/plain"
|
||||
|
||||
try:
|
||||
response = requests.request(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
json=json_data,
|
||||
data=data,
|
||||
headers=request_headers,
|
||||
timeout=config.request_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
parsed_json = response.json()
|
||||
|
||||
# Add timestamp if not present
|
||||
if isinstance(parsed_json, dict) and "timestamp" not in parsed_json:
|
||||
parsed_json["timestamp"] = int(time.time() * 1000)
|
||||
|
||||
# Normalize error format
|
||||
if (
|
||||
not response.ok
|
||||
and isinstance(parsed_json, dict)
|
||||
and "success" in parsed_json
|
||||
and not parsed_json["success"]
|
||||
):
|
||||
if "error" in parsed_json and not isinstance(
|
||||
parsed_json["error"], dict
|
||||
):
|
||||
error_message = parsed_json["error"]
|
||||
parsed_json["error"] = {
|
||||
"code": f"HTTP_{response.status_code}",
|
||||
"message": error_message,
|
||||
}
|
||||
|
||||
return parsed_json
|
||||
|
||||
except ValueError:
|
||||
if response.ok:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "NON_JSON_RESPONSE",
|
||||
"message": "Received non-JSON success response",
|
||||
},
|
||||
"status_code": response.status_code,
|
||||
"response_text": response.text[:500],
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": f"HTTP_{response.status_code}",
|
||||
"message": f"Non-JSON error: {response.text[:100]}...",
|
||||
},
|
||||
"status_code": response.status_code,
|
||||
"response_text": response.text[:500],
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "REQUEST_TIMEOUT", "message": "Request timed out"},
|
||||
"status_code": 408,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "CONNECTION_ERROR",
|
||||
"message": f"Failed to connect to Ghidra instance at {url}",
|
||||
},
|
||||
"status_code": 503,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "UNEXPECTED_ERROR",
|
||||
"message": f"Unexpected error: {str(e)}",
|
||||
},
|
||||
"exception": e.__class__.__name__,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
|
||||
def safe_get(
|
||||
port: int, endpoint: str, params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make GET request to Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
params: Query parameters
|
||||
|
||||
Returns:
|
||||
Response dict
|
||||
"""
|
||||
return _make_request("GET", port, endpoint, params=params)
|
||||
|
||||
|
||||
def safe_post(
|
||||
port: int, endpoint: str, data: Union[Dict[str, Any], str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Make POST request to Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
data: JSON dict or raw string payload
|
||||
|
||||
Returns:
|
||||
Response dict
|
||||
"""
|
||||
headers = None
|
||||
json_payload = None
|
||||
text_payload = None
|
||||
|
||||
if isinstance(data, dict):
|
||||
data = data.copy() # Don't mutate caller's dict
|
||||
headers = data.pop("headers", None)
|
||||
json_payload = data
|
||||
else:
|
||||
text_payload = data
|
||||
|
||||
return _make_request(
|
||||
"POST", port, endpoint, json_data=json_payload, data=text_payload, headers=headers
|
||||
)
|
||||
|
||||
|
||||
def safe_put(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Make PUT request to Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
data: JSON payload
|
||||
|
||||
Returns:
|
||||
Response dict
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def safe_patch(port: int, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Make PATCH request to Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
data: JSON payload
|
||||
|
||||
Returns:
|
||||
Response dict
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def safe_delete(port: int, endpoint: str) -> Dict[str, Any]:
|
||||
"""Make DELETE request to Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
endpoint: API endpoint path
|
||||
|
||||
Returns:
|
||||
Response dict
|
||||
"""
|
||||
return _make_request("DELETE", port, endpoint)
|
||||
|
||||
|
||||
def simplify_response(response: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Simplify HATEOAS response for MCP tool consumption.
|
||||
|
||||
- Removes _links from result entries
|
||||
- Flattens nested structures
|
||||
- Preserves important metadata
|
||||
- Converts structured data to text
|
||||
|
||||
Args:
|
||||
response: Raw API response
|
||||
|
||||
Returns:
|
||||
Simplified response dict
|
||||
"""
|
||||
if not isinstance(response, dict):
|
||||
return response
|
||||
|
||||
result = response.copy()
|
||||
|
||||
# Store API metadata
|
||||
api_metadata = {}
|
||||
for key in ["id", "instance", "timestamp", "size", "offset", "limit"]:
|
||||
if key in result:
|
||||
api_metadata[key] = result.get(key)
|
||||
|
||||
# Simplify result data
|
||||
if "result" in result:
|
||||
if isinstance(result["result"], list):
|
||||
simplified_items = []
|
||||
for item in result["result"]:
|
||||
if isinstance(item, dict):
|
||||
item_copy = item.copy()
|
||||
links = item_copy.pop("_links", None)
|
||||
if isinstance(links, dict):
|
||||
for link_name, link_data in links.items():
|
||||
if isinstance(link_data, dict) and "href" in link_data:
|
||||
item_copy[f"{link_name}_url"] = link_data["href"]
|
||||
simplified_items.append(item_copy)
|
||||
else:
|
||||
simplified_items.append(item)
|
||||
result["result"] = simplified_items
|
||||
|
||||
elif isinstance(result["result"], dict):
|
||||
result_copy = result["result"].copy()
|
||||
links = result_copy.pop("_links", None)
|
||||
|
||||
if isinstance(links, dict):
|
||||
for link_name, link_data in links.items():
|
||||
if isinstance(link_data, dict) and "href" in link_data:
|
||||
result_copy[f"{link_name}_url"] = link_data["href"]
|
||||
|
||||
# Convert disassembly to text
|
||||
if "instructions" in result_copy and isinstance(
|
||||
result_copy["instructions"], list
|
||||
):
|
||||
disasm_text = ""
|
||||
for instr in result_copy["instructions"]:
|
||||
if isinstance(instr, dict):
|
||||
addr = instr.get("address", "")
|
||||
mnemonic = instr.get("mnemonic", "")
|
||||
operands = instr.get("operands", "")
|
||||
bytes_str = instr.get("bytes", "")
|
||||
disasm_text += (
|
||||
f"{addr}: {bytes_str.ljust(10)} {mnemonic} {operands}\n"
|
||||
)
|
||||
result_copy["disassembly_text"] = disasm_text
|
||||
result_copy.pop("instructions", None)
|
||||
|
||||
# Make decompiled code accessible
|
||||
if "ccode" in result_copy:
|
||||
result_copy["decompiled_text"] = result_copy["ccode"]
|
||||
elif "decompiled" in result_copy:
|
||||
result_copy["decompiled_text"] = result_copy["decompiled"]
|
||||
|
||||
result["result"] = result_copy
|
||||
|
||||
# Simplify top-level links
|
||||
links = result.pop("_links", None)
|
||||
if isinstance(links, dict):
|
||||
api_links = {}
|
||||
for link_name, link_data in links.items():
|
||||
if isinstance(link_data, dict) and "href" in link_data:
|
||||
api_links[link_name] = link_data["href"]
|
||||
if api_links:
|
||||
result["api_links"] = api_links
|
||||
|
||||
# Restore metadata
|
||||
for key, value in api_metadata.items():
|
||||
if key not in result:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
88
src/mcghidra/core/logging.py
Normal file
88
src/mcghidra/core/logging.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""Logging utilities for MCP context-aware logging.
|
||||
|
||||
Provides async logging functions that use FastMCP's Context for
|
||||
client-visible logging when available, with fallback to standard logging.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp import Context
|
||||
|
||||
# Standard Python logger as fallback
|
||||
logger = logging.getLogger("mcghidra")
|
||||
|
||||
|
||||
async def log_debug(ctx: Optional["Context"], message: str) -> None:
|
||||
"""Log a debug message to the MCP client and/or standard logger.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
message: Debug message to log
|
||||
"""
|
||||
logger.debug(message)
|
||||
if ctx is not None:
|
||||
try:
|
||||
await ctx.debug(message)
|
||||
except Exception:
|
||||
pass # Silently ignore if context doesn't support logging
|
||||
|
||||
|
||||
async def log_info(ctx: Optional["Context"], message: str) -> None:
|
||||
"""Log an info message to the MCP client and/or standard logger.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
message: Info message to log
|
||||
"""
|
||||
logger.info(message)
|
||||
if ctx is not None:
|
||||
try:
|
||||
await ctx.info(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def log_warning(ctx: Optional["Context"], message: str) -> None:
|
||||
"""Log a warning message to the MCP client and/or standard logger.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
message: Warning message to log
|
||||
"""
|
||||
logger.warning(message)
|
||||
if ctx is not None:
|
||||
try:
|
||||
await ctx.warning(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def log_error(ctx: Optional["Context"], message: str) -> None:
|
||||
"""Log an error message to the MCP client and/or standard logger.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
message: Error message to log
|
||||
"""
|
||||
logger.error(message)
|
||||
if ctx is not None:
|
||||
try:
|
||||
await ctx.error(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def configure_logging(level: int = logging.INFO) -> None:
|
||||
"""Configure the standard logger for MCGhidra.
|
||||
|
||||
Args:
|
||||
level: Logging level (default: INFO)
|
||||
"""
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(level)
|
||||
529
src/mcghidra/core/pagination.py
Normal file
529
src/mcghidra/core/pagination.py
Normal file
@ -0,0 +1,529 @@
|
||||
"""Cursor-based pagination system for large MCP responses.
|
||||
|
||||
Provides efficient pagination with grep filtering, session isolation,
|
||||
and TTL-based cursor expiration.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ..config import get_config
|
||||
from .filtering import estimate_and_guard, project_fields
|
||||
|
||||
# ReDoS Protection Configuration
|
||||
MAX_GREP_PATTERN_LENGTH = 500
|
||||
MAX_GREP_REPETITION_OPS = 15
|
||||
MAX_GREP_RECURSION_DEPTH = 10
|
||||
|
||||
# Token estimation (roughly 4 chars per token)
|
||||
TOKEN_ESTIMATION_RATIO = 4.0
|
||||
|
||||
|
||||
def compile_safe_pattern(pattern: str, flags: int = 0) -> re.Pattern:
|
||||
"""Compile regex pattern with ReDoS protection.
|
||||
|
||||
Validates pattern to prevent catastrophic backtracking attacks.
|
||||
|
||||
Args:
|
||||
pattern: Regex pattern string
|
||||
flags: Regex compilation flags
|
||||
|
||||
Returns:
|
||||
Compiled regex pattern
|
||||
|
||||
Raises:
|
||||
ValueError: If pattern fails safety validation
|
||||
"""
|
||||
if not pattern:
|
||||
raise ValueError("Empty pattern")
|
||||
|
||||
if len(pattern) > MAX_GREP_PATTERN_LENGTH:
|
||||
raise ValueError(
|
||||
f"Pattern too long ({len(pattern)} chars, max {MAX_GREP_PATTERN_LENGTH}). "
|
||||
"Consider using a simpler pattern."
|
||||
)
|
||||
|
||||
# Count repetition operators
|
||||
repetition_ops = pattern.count("*") + pattern.count("+") + pattern.count("?")
|
||||
repetition_ops += len(re.findall(r"\{[0-9,]+\}", pattern))
|
||||
|
||||
if repetition_ops > MAX_GREP_REPETITION_OPS:
|
||||
raise ValueError(
|
||||
f"Pattern has too many repetition operators ({repetition_ops}, "
|
||||
f"max {MAX_GREP_REPETITION_OPS}). Consider simplifying."
|
||||
)
|
||||
|
||||
# Check for dangerous nested quantifiers
|
||||
dangerous_patterns = [
|
||||
r"\([^)]*[*+][^)]*\)[*+]", # (a+)+ or (a*)*
|
||||
r"\([^)]*[*+][^)]*\)\{", # (a+){n,m}
|
||||
]
|
||||
for dangerous in dangerous_patterns:
|
||||
if re.search(dangerous, pattern):
|
||||
raise ValueError(
|
||||
"Pattern contains nested quantifiers which could cause "
|
||||
"exponential backtracking. Consider simplifying."
|
||||
)
|
||||
|
||||
try:
|
||||
return re.compile(pattern, flags)
|
||||
except re.error as e:
|
||||
raise ValueError(f"Invalid regex pattern: {e}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CursorState:
|
||||
"""Represents the state of a paginated query with session isolation."""
|
||||
|
||||
cursor_id: str
|
||||
session_id: str
|
||||
tool_name: str
|
||||
query_hash: str
|
||||
data: List[Any]
|
||||
total_count: int
|
||||
filtered_count: int
|
||||
current_offset: int = 0
|
||||
page_size: int = 50
|
||||
grep_pattern: Optional[str] = None
|
||||
grep_flags: int = 0
|
||||
created_at: float = field(default_factory=time.time)
|
||||
last_accessed: float = field(default_factory=time.time)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
config = get_config()
|
||||
return time.time() - self.last_accessed > config.cursor_ttl_seconds
|
||||
|
||||
@property
|
||||
def has_more(self) -> bool:
|
||||
return self.current_offset + self.page_size < self.filtered_count
|
||||
|
||||
@property
|
||||
def current_page(self) -> int:
|
||||
return (self.current_offset // self.page_size) + 1
|
||||
|
||||
@property
|
||||
def total_pages(self) -> int:
|
||||
return max(1, (self.filtered_count + self.page_size - 1) // self.page_size)
|
||||
|
||||
@property
|
||||
def ttl_remaining(self) -> int:
|
||||
config = get_config()
|
||||
return max(0, int(config.cursor_ttl_seconds - (time.time() - self.last_accessed)))
|
||||
|
||||
def verify_session(self, session_id: str) -> bool:
|
||||
"""Verify cursor belongs to requesting session."""
|
||||
return self.session_id == session_id
|
||||
|
||||
|
||||
class CursorManager:
|
||||
"""Thread-safe cursor manager with TTL-based expiration and session isolation."""
|
||||
|
||||
def __init__(self):
|
||||
self._cursors: OrderedDict[str, CursorState] = OrderedDict()
|
||||
self._session_cursors: Dict[str, set] = {}
|
||||
self._lock = Lock()
|
||||
|
||||
def _generate_cursor_id(self, query_hash: str, session_id: str) -> str:
|
||||
"""Generate a unique cursor ID."""
|
||||
unique = f"{session_id}-{query_hash}-{time.time()}-{id(self)}"
|
||||
return hashlib.sha256(unique.encode()).hexdigest()[:16]
|
||||
|
||||
def _cleanup_expired(self) -> None:
|
||||
"""Remove expired cursors (call while holding lock)."""
|
||||
config = get_config()
|
||||
|
||||
expired = [cid for cid, state in self._cursors.items() if state.is_expired]
|
||||
for cid in expired:
|
||||
state = self._cursors[cid]
|
||||
if state.session_id in self._session_cursors:
|
||||
self._session_cursors[state.session_id].discard(cid)
|
||||
del self._cursors[cid]
|
||||
|
||||
# LRU eviction
|
||||
while len(self._cursors) > config.max_cursors_per_session:
|
||||
oldest_id, oldest_state = self._cursors.popitem(last=False)
|
||||
if oldest_state.session_id in self._session_cursors:
|
||||
self._session_cursors[oldest_state.session_id].discard(oldest_id)
|
||||
|
||||
def create_cursor(
|
||||
self,
|
||||
data: List[Any],
|
||||
query_params: Dict[str, Any],
|
||||
tool_name: str = "unknown",
|
||||
session_id: str = "default",
|
||||
grep_pattern: Optional[str] = None,
|
||||
grep_flags: int = 0,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[str, CursorState]:
|
||||
"""Create a new cursor for paginated results.
|
||||
|
||||
Args:
|
||||
data: The full result set to paginate
|
||||
query_params: Original query parameters (for hashing)
|
||||
tool_name: Name of tool creating cursor
|
||||
session_id: Session identifier for isolation
|
||||
grep_pattern: Optional regex pattern to filter results
|
||||
grep_flags: Regex flags
|
||||
page_size: Items per page
|
||||
|
||||
Returns:
|
||||
Tuple of (cursor_id, cursor_state)
|
||||
"""
|
||||
config = get_config()
|
||||
|
||||
# Apply grep filtering
|
||||
filtered_data = data
|
||||
if grep_pattern:
|
||||
pattern = compile_safe_pattern(grep_pattern, grep_flags)
|
||||
filtered_data = [
|
||||
item for item in data if self._matches_grep(item, pattern)
|
||||
]
|
||||
|
||||
# Create query hash (SHA-256 for consistency with cursor ID generation)
|
||||
query_hash = hashlib.sha256(
|
||||
json.dumps(query_params, sort_keys=True, default=str).encode()
|
||||
).hexdigest()[:12]
|
||||
|
||||
with self._lock:
|
||||
self._cleanup_expired()
|
||||
|
||||
cursor_id = self._generate_cursor_id(query_hash, session_id)
|
||||
state = CursorState(
|
||||
cursor_id=cursor_id,
|
||||
session_id=session_id,
|
||||
tool_name=tool_name,
|
||||
query_hash=query_hash,
|
||||
data=filtered_data,
|
||||
total_count=len(data),
|
||||
filtered_count=len(filtered_data),
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep_pattern=grep_pattern,
|
||||
grep_flags=grep_flags,
|
||||
)
|
||||
self._cursors[cursor_id] = state
|
||||
|
||||
if session_id not in self._session_cursors:
|
||||
self._session_cursors[session_id] = set()
|
||||
self._session_cursors[session_id].add(cursor_id)
|
||||
|
||||
return cursor_id, state
|
||||
|
||||
def get_cursor(
|
||||
self, cursor_id: str, session_id: Optional[str] = None
|
||||
) -> Optional[CursorState]:
|
||||
"""Retrieve a cursor by ID, optionally validating session."""
|
||||
with self._lock:
|
||||
self._cleanup_expired()
|
||||
|
||||
if cursor_id not in self._cursors:
|
||||
return None
|
||||
|
||||
state = self._cursors[cursor_id]
|
||||
if state.is_expired:
|
||||
del self._cursors[cursor_id]
|
||||
if state.session_id in self._session_cursors:
|
||||
self._session_cursors[state.session_id].discard(cursor_id)
|
||||
return None
|
||||
|
||||
if session_id and not state.verify_session(session_id):
|
||||
return None
|
||||
|
||||
state.last_accessed = time.time()
|
||||
self._cursors.move_to_end(cursor_id)
|
||||
return state
|
||||
|
||||
def advance_cursor(
|
||||
self, cursor_id: str, session_id: Optional[str] = None
|
||||
) -> Optional[CursorState]:
|
||||
"""Advance cursor to next page."""
|
||||
with self._lock:
|
||||
state = self._cursors.get(cursor_id)
|
||||
if not state or state.is_expired:
|
||||
return None
|
||||
|
||||
if session_id and not state.verify_session(session_id):
|
||||
return None
|
||||
|
||||
state.current_offset += state.page_size
|
||||
state.last_accessed = time.time()
|
||||
self._cursors.move_to_end(cursor_id)
|
||||
return state
|
||||
|
||||
def delete_cursor(
|
||||
self, cursor_id: str, session_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Explicitly delete a cursor."""
|
||||
with self._lock:
|
||||
if cursor_id not in self._cursors:
|
||||
return False
|
||||
|
||||
state = self._cursors[cursor_id]
|
||||
if session_id and not state.verify_session(session_id):
|
||||
return False
|
||||
|
||||
if state.session_id in self._session_cursors:
|
||||
self._session_cursors[state.session_id].discard(cursor_id)
|
||||
del self._cursors[cursor_id]
|
||||
return True
|
||||
|
||||
def delete_session_cursors(self, session_id: str) -> int:
|
||||
"""Delete all cursors for a session."""
|
||||
with self._lock:
|
||||
if session_id not in self._session_cursors:
|
||||
return 0
|
||||
|
||||
cursor_ids = list(self._session_cursors[session_id])
|
||||
count = 0
|
||||
for cid in cursor_ids:
|
||||
if cid in self._cursors:
|
||||
del self._cursors[cid]
|
||||
count += 1
|
||||
del self._session_cursors[session_id]
|
||||
return count
|
||||
|
||||
def get_page(self, state: CursorState) -> List[Any]:
|
||||
"""Get current page of data from cursor state."""
|
||||
start = state.current_offset
|
||||
end = start + state.page_size
|
||||
return state.data[start:end]
|
||||
|
||||
def _matches_grep(
|
||||
self, item: Any, pattern: re.Pattern, depth: int = 0
|
||||
) -> bool:
|
||||
"""Check if an item matches the grep pattern.
|
||||
|
||||
Searches through string representations of dict values,
|
||||
list items, or the item itself.
|
||||
"""
|
||||
if depth > MAX_GREP_RECURSION_DEPTH:
|
||||
return False
|
||||
|
||||
if isinstance(item, dict):
|
||||
for value in item.values():
|
||||
if isinstance(value, str) and pattern.search(value):
|
||||
return True
|
||||
elif isinstance(value, (int, float)):
|
||||
if pattern.search(str(value)):
|
||||
return True
|
||||
elif isinstance(value, dict):
|
||||
if self._matches_grep(value, pattern, depth + 1):
|
||||
return True
|
||||
elif isinstance(value, (list, tuple)):
|
||||
if self._matches_grep(value, pattern, depth + 1):
|
||||
return True
|
||||
return False
|
||||
elif isinstance(item, (list, tuple)):
|
||||
return any(self._matches_grep(i, pattern, depth + 1) for i in item)
|
||||
elif isinstance(item, str):
|
||||
return bool(pattern.search(item))
|
||||
else:
|
||||
return bool(pattern.search(str(item)))
|
||||
|
||||
def list_cursors(self, session_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""List active cursors, optionally filtered by session."""
|
||||
with self._lock:
|
||||
self._cleanup_expired()
|
||||
return [
|
||||
{
|
||||
"cursor_id": cid,
|
||||
"session_id": state.session_id,
|
||||
"tool_name": state.tool_name,
|
||||
"total_count": state.total_count,
|
||||
"filtered_count": state.filtered_count,
|
||||
"current_page": state.current_page,
|
||||
"total_pages": state.total_pages,
|
||||
"current_offset": state.current_offset,
|
||||
"page_size": state.page_size,
|
||||
"has_more": state.has_more,
|
||||
"grep_pattern": state.grep_pattern,
|
||||
"age_seconds": int(time.time() - state.created_at),
|
||||
"ttl_remaining": state.ttl_remaining,
|
||||
}
|
||||
for cid, state in self._cursors.items()
|
||||
if session_id is None or state.session_id == session_id
|
||||
]
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get cursor manager statistics."""
|
||||
config = get_config()
|
||||
with self._lock:
|
||||
self._cleanup_expired()
|
||||
return {
|
||||
"total_cursors": len(self._cursors),
|
||||
"total_sessions": len(self._session_cursors),
|
||||
"max_cache_size": config.max_cursors_per_session,
|
||||
"ttl_seconds": config.cursor_ttl_seconds,
|
||||
"cursors_per_session": {
|
||||
sid: len(cids) for sid, cids in self._session_cursors.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Global cursor manager instance
|
||||
_cursor_manager: Optional[CursorManager] = None
|
||||
|
||||
|
||||
def get_cursor_manager() -> CursorManager:
|
||||
"""Get the global cursor manager instance."""
|
||||
global _cursor_manager
|
||||
if _cursor_manager is None:
|
||||
_cursor_manager = CursorManager()
|
||||
return _cursor_manager
|
||||
|
||||
|
||||
def estimate_tokens(data: List[Any]) -> int:
|
||||
"""Estimate token count for a list of items."""
|
||||
text = json.dumps(data, default=str)
|
||||
return int(len(text) / TOKEN_ESTIMATION_RATIO)
|
||||
|
||||
|
||||
def paginate_response(
|
||||
data: List[Any],
|
||||
query_params: Dict[str, Any],
|
||||
tool_name: str = "unknown",
|
||||
session_id: str = "default",
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a paginated response with optional grep filtering and field projection.
|
||||
|
||||
Args:
|
||||
data: Full result list to paginate
|
||||
query_params: Original query parameters (for cursor creation)
|
||||
tool_name: Name of the tool creating this response
|
||||
session_id: Session identifier for cursor isolation
|
||||
page_size: Items per page (default: 50, max: 500)
|
||||
grep: Optional regex pattern to filter results
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Bypass pagination and return all results (with budget guard)
|
||||
fields: Optional list of field names to project (jq-style)
|
||||
|
||||
Returns:
|
||||
dict with pagination metadata and results
|
||||
"""
|
||||
config = get_config()
|
||||
cursor_manager = get_cursor_manager()
|
||||
grep_flags = re.IGNORECASE if grep_ignorecase else 0
|
||||
|
||||
# Handle return_all bypass
|
||||
if return_all:
|
||||
filtered_data = data
|
||||
if grep:
|
||||
try:
|
||||
pattern = compile_safe_pattern(grep, grep_flags)
|
||||
filtered_data = [
|
||||
item
|
||||
for item in data
|
||||
if cursor_manager._matches_grep(item, pattern)
|
||||
]
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "INVALID_GREP_PATTERN", "message": str(e)},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
# Apply field projection before size estimation
|
||||
if fields:
|
||||
filtered_data = project_fields(filtered_data, fields)
|
||||
|
||||
# Check token budget — return guard if exceeded
|
||||
guard = estimate_and_guard(
|
||||
data=filtered_data,
|
||||
tool_name=tool_name,
|
||||
query_hints=query_params,
|
||||
)
|
||||
if guard is not None:
|
||||
return guard
|
||||
|
||||
estimated_tokens = estimate_tokens(filtered_data)
|
||||
warning = None
|
||||
|
||||
if estimated_tokens > 50000:
|
||||
warning = f"EXTREMELY LARGE response (~{estimated_tokens:,} tokens)"
|
||||
elif estimated_tokens > 20000:
|
||||
warning = f"VERY LARGE response (~{estimated_tokens:,} tokens)"
|
||||
elif estimated_tokens > config.large_response_threshold:
|
||||
warning = f"Large response (~{estimated_tokens:,} tokens)"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": filtered_data,
|
||||
"pagination": {
|
||||
"bypassed": True,
|
||||
"total_count": len(data),
|
||||
"filtered_count": len(filtered_data),
|
||||
"grep_pattern": grep,
|
||||
"fields_projected": fields,
|
||||
"estimated_tokens": estimated_tokens,
|
||||
"warning": warning,
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
# Normal pagination flow — apply field projection before cursoring
|
||||
paginated_data = project_fields(data, fields) if fields else data
|
||||
|
||||
try:
|
||||
cursor_id, state = cursor_manager.create_cursor(
|
||||
data=paginated_data,
|
||||
query_params=query_params,
|
||||
tool_name=tool_name,
|
||||
session_id=session_id,
|
||||
grep_pattern=grep,
|
||||
grep_flags=grep_flags,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
)
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {"code": "INVALID_GREP_PATTERN", "message": str(e)},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
current_page = cursor_manager.get_page(state)
|
||||
response_cursor = cursor_id if state.has_more else None
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"result": current_page,
|
||||
"pagination": {
|
||||
"cursor_id": response_cursor,
|
||||
"session_id": session_id,
|
||||
"total_count": state.total_count,
|
||||
"filtered_count": state.filtered_count,
|
||||
"page_size": state.page_size,
|
||||
"current_page": state.current_page,
|
||||
"total_pages": state.total_pages,
|
||||
"has_more": state.has_more,
|
||||
"grep_pattern": grep,
|
||||
"items_returned": len(current_page),
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
|
||||
# Add LLM-friendly continuation message
|
||||
if state.has_more:
|
||||
remaining = state.filtered_count - (state.current_page * state.page_size)
|
||||
response["_message"] = (
|
||||
f"Showing {len(current_page)} of {state.filtered_count} items "
|
||||
f"(page {state.current_page}/{state.total_pages}). "
|
||||
f"To get the next {min(state.page_size, remaining)} items, call: "
|
||||
f"cursor_next(cursor_id='{cursor_id}')"
|
||||
)
|
||||
else:
|
||||
response["_message"] = (
|
||||
f"Complete: {len(current_page)} items returned (all results)"
|
||||
)
|
||||
|
||||
return response
|
||||
161
src/mcghidra/core/progress.py
Normal file
161
src/mcghidra/core/progress.py
Normal file
@ -0,0 +1,161 @@
|
||||
"""Progress reporting utilities for long-running operations.
|
||||
|
||||
Provides async progress reporting using FastMCP's Context for
|
||||
real-time progress notifications to MCP clients.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
|
||||
class ProgressReporter:
|
||||
"""Helper class for reporting progress during long operations.
|
||||
|
||||
Provides throttled progress updates to avoid spamming the client
|
||||
with too many notifications.
|
||||
|
||||
Example:
|
||||
async def long_operation(ctx: Context):
|
||||
progress = ProgressReporter(ctx, "Scanning", total=100)
|
||||
for i in range(100):
|
||||
await progress.update(message=f"Processing item {i}")
|
||||
await progress.complete("Scan finished")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ctx: Optional["Context"],
|
||||
operation: str,
|
||||
total: int = 100
|
||||
):
|
||||
"""Initialize the progress reporter.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context for progress reporting (may be None)
|
||||
operation: Name of the operation (used in log messages)
|
||||
total: Total number of steps (default: 100)
|
||||
"""
|
||||
self.ctx = ctx
|
||||
self.operation = operation
|
||||
self.total = total
|
||||
self.current = 0
|
||||
self._last_reported = 0
|
||||
# Report every 5% at minimum
|
||||
self._report_threshold = max(1, total // 20)
|
||||
|
||||
async def update(
|
||||
self,
|
||||
progress: Optional[int] = None,
|
||||
message: Optional[str] = None
|
||||
) -> None:
|
||||
"""Update progress, reporting to client if threshold reached.
|
||||
|
||||
Args:
|
||||
progress: Current progress value (if None, increments by 1)
|
||||
message: Optional message to log with the progress update
|
||||
"""
|
||||
if progress is not None:
|
||||
self.current = progress
|
||||
else:
|
||||
self.current += 1
|
||||
|
||||
# Only report if we've crossed a threshold or reached the end
|
||||
should_report = (
|
||||
self.current - self._last_reported >= self._report_threshold
|
||||
or self.current >= self.total
|
||||
)
|
||||
|
||||
if self.ctx and should_report:
|
||||
try:
|
||||
await self.ctx.report_progress(
|
||||
progress=self.current,
|
||||
total=self.total
|
||||
)
|
||||
if message:
|
||||
await self.ctx.info(f"{self.operation}: {message}")
|
||||
self._last_reported = self.current
|
||||
except Exception:
|
||||
pass # Silently ignore if context doesn't support progress
|
||||
|
||||
async def info(self, message: str) -> None:
|
||||
"""Send an info message to the client.
|
||||
|
||||
Args:
|
||||
message: Message to send
|
||||
"""
|
||||
if self.ctx:
|
||||
try:
|
||||
await self.ctx.info(f"{self.operation}: {message}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def complete(self, message: Optional[str] = None) -> None:
|
||||
"""Mark operation as complete.
|
||||
|
||||
Args:
|
||||
message: Optional completion message (supports format placeholders:
|
||||
{count}, {total}, {operation})
|
||||
"""
|
||||
self.current = self.total
|
||||
if self.ctx:
|
||||
try:
|
||||
await self.ctx.report_progress(
|
||||
progress=self.total,
|
||||
total=self.total
|
||||
)
|
||||
if message:
|
||||
formatted = message.format(
|
||||
count=self.current,
|
||||
total=self.total,
|
||||
operation=self.operation
|
||||
)
|
||||
await self.ctx.info(formatted)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def report_progress(
|
||||
ctx: Optional["Context"],
|
||||
progress: int,
|
||||
total: int,
|
||||
message: Optional[str] = None
|
||||
) -> None:
|
||||
"""Convenience function for one-off progress updates.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
progress: Current progress value
|
||||
total: Total progress value
|
||||
message: Optional message to log
|
||||
"""
|
||||
if ctx:
|
||||
try:
|
||||
await ctx.report_progress(progress=progress, total=total)
|
||||
if message:
|
||||
await ctx.info(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def report_step(
|
||||
ctx: Optional["Context"],
|
||||
step: int,
|
||||
total_steps: int,
|
||||
description: str
|
||||
) -> None:
|
||||
"""Report a discrete step in a multi-step operation.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (may be None)
|
||||
step: Current step number (1-indexed)
|
||||
total_steps: Total number of steps
|
||||
description: Description of the current step
|
||||
"""
|
||||
if ctx:
|
||||
try:
|
||||
await ctx.report_progress(progress=step, total=total_steps)
|
||||
await ctx.info(f"Step {step}/{total_steps}: {description}")
|
||||
except Exception:
|
||||
pass
|
||||
41
src/mcghidra/mixins/__init__.py
Normal file
41
src/mcghidra/mixins/__init__.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""MCP Mixins for MCGhidra.
|
||||
|
||||
Domain-specific mixins that organize tools, resources, and prompts by functionality.
|
||||
Uses FastMCP's contrib.mcp_mixin pattern for clean modular organization.
|
||||
"""
|
||||
|
||||
from .analysis import AnalysisMixin
|
||||
from .base import MCGhidraMixinBase
|
||||
from .bookmarks import BookmarksMixin
|
||||
from .cursors import CursorsMixin
|
||||
from .data import DataMixin
|
||||
from .datatypes import DataTypesMixin
|
||||
from .docker import DockerMixin
|
||||
from .functions import FunctionsMixin
|
||||
from .instances import InstancesMixin
|
||||
from .memory import MemoryMixin
|
||||
from .namespaces import NamespacesMixin
|
||||
from .segments import SegmentsMixin
|
||||
from .structs import StructsMixin
|
||||
from .symbols import SymbolsMixin
|
||||
from .variables import VariablesMixin
|
||||
from .xrefs import XrefsMixin
|
||||
|
||||
__all__ = [
|
||||
"MCGhidraMixinBase",
|
||||
"InstancesMixin",
|
||||
"FunctionsMixin",
|
||||
"DataMixin",
|
||||
"StructsMixin",
|
||||
"AnalysisMixin",
|
||||
"MemoryMixin",
|
||||
"XrefsMixin",
|
||||
"CursorsMixin",
|
||||
"DockerMixin",
|
||||
"SymbolsMixin",
|
||||
"SegmentsMixin",
|
||||
"VariablesMixin",
|
||||
"NamespacesMixin",
|
||||
"BookmarksMixin",
|
||||
"DataTypesMixin",
|
||||
]
|
||||
372
src/mcghidra/mixins/analysis.py
Normal file
372
src/mcghidra/mixins/analysis.py
Normal file
@ -0,0 +1,372 @@
|
||||
"""Analysis mixin for MCGhidra.
|
||||
|
||||
Provides tools for program analysis operations.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from ..core.logging import logger
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class AnalysisMixin(MCGhidraMixinBase):
|
||||
"""Mixin for analysis operations.
|
||||
|
||||
Provides tools for:
|
||||
- Running program analysis
|
||||
- Call graph analysis
|
||||
- Data flow analysis
|
||||
- UI state queries
|
||||
- Comment management
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def analysis_run(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
analysis_options: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Run analysis on the current program.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
analysis_options: Analysis options to enable/disable
|
||||
|
||||
Returns:
|
||||
Analysis operation result
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
response = self.safe_post(port, "analysis", analysis_options or {})
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def analysis_get_callgraph(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
max_depth: int = 3,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get function call graph with edge pagination.
|
||||
|
||||
Args:
|
||||
name: Starting function name (mutually exclusive with address)
|
||||
address: Starting function address
|
||||
max_depth: Maximum call depth (default: 3)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Edges per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter edges
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all edges without pagination
|
||||
fields: Field names to keep per edge (e.g. ['from', 'to']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Call graph with paginated edges
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
params = {"max_depth": max_depth}
|
||||
if address:
|
||||
params["address"] = address
|
||||
func_id = address
|
||||
elif name:
|
||||
params["name"] = name
|
||||
func_id = name
|
||||
else:
|
||||
func_id = "entry_point"
|
||||
|
||||
response = self.safe_get(port, "analysis/callgraph", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result = simplified.get("result", {})
|
||||
edges = result.get("edges", []) if isinstance(result, dict) else []
|
||||
nodes = result.get("nodes", []) if isinstance(result, dict) else []
|
||||
|
||||
if not edges:
|
||||
return simplified
|
||||
|
||||
query_params = {
|
||||
"tool": "analysis_get_callgraph",
|
||||
"port": port,
|
||||
"name": name,
|
||||
"address": address,
|
||||
"max_depth": max_depth,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
paginated = self.filtered_paginate(
|
||||
data=edges,
|
||||
query_params=query_params,
|
||||
tool_name="analysis_get_callgraph",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
if paginated.get("success") and not paginated.get("guarded"):
|
||||
paginated["result"] = {
|
||||
"root_function": func_id,
|
||||
"max_depth": max_depth,
|
||||
"nodes": nodes,
|
||||
"edges": paginated.get("result", []),
|
||||
"total_nodes": len(nodes),
|
||||
}
|
||||
|
||||
return paginated
|
||||
|
||||
@mcp_tool()
|
||||
def analysis_get_dataflow(
|
||||
self,
|
||||
address: str,
|
||||
direction: str = "forward",
|
||||
max_steps: int = 50,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Perform data flow analysis with step pagination.
|
||||
|
||||
Args:
|
||||
address: Starting address in hex format
|
||||
direction: "forward" or "backward" (default: "forward")
|
||||
max_steps: Maximum analysis steps (default: 50)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Steps per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter steps
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all steps without pagination
|
||||
fields: Field names to keep per step. Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Data flow steps with pagination
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
params = {
|
||||
"address": address,
|
||||
"direction": direction,
|
||||
"max_steps": max_steps,
|
||||
}
|
||||
|
||||
response = self.safe_get(port, "analysis/dataflow", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result = simplified.get("result", {})
|
||||
steps = result.get("steps", []) if isinstance(result, dict) else []
|
||||
|
||||
if not steps:
|
||||
return simplified
|
||||
|
||||
query_params = {
|
||||
"tool": "analysis_get_dataflow",
|
||||
"port": port,
|
||||
"address": address,
|
||||
"direction": direction,
|
||||
"max_steps": max_steps,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
paginated = self.filtered_paginate(
|
||||
data=steps,
|
||||
query_params=query_params,
|
||||
tool_name="analysis_get_dataflow",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Merge metadata into result (skip if guarded)
|
||||
if paginated.get("success") and not paginated.get("guarded"):
|
||||
paginated["result"] = {
|
||||
"start_address": address,
|
||||
"direction": direction,
|
||||
"steps": paginated.get("result", []),
|
||||
}
|
||||
if isinstance(result, dict):
|
||||
for key in ["sources", "sinks", "total_steps"]:
|
||||
if key in result:
|
||||
paginated["result"][key] = result[key]
|
||||
|
||||
return paginated
|
||||
|
||||
# NOTE: ui_get_current_address and ui_get_current_function were removed
|
||||
# because they require Ghidra GUI context which is never available in
|
||||
# headless MCP mode. Use functions_get(address=...) or data_list(addr=...)
|
||||
# with explicit addresses instead.
|
||||
|
||||
@mcp_tool()
|
||||
def comments_get(
|
||||
self,
|
||||
address: str,
|
||||
comment_type: str = "plate",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a comment at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
comment_type: "plate", "pre", "post", "eol", "repeatable"
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Comment text and metadata
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
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, f"memory/{address}/comments/{comment_type}")
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def comments_set(
|
||||
self,
|
||||
address: str,
|
||||
comment: str = "",
|
||||
comment_type: str = "plate",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Set a comment at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
comment: Comment text (empty string removes comment)
|
||||
comment_type: "plate", "pre", "post", "eol", "repeatable"
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"comment": comment}
|
||||
response = self.safe_post(port, f"memory/{address}/comments/{comment_type}", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_set_comment(
|
||||
self,
|
||||
address: str,
|
||||
comment: str = "",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Set a decompiler-friendly comment (function comment with fallback).
|
||||
|
||||
Args:
|
||||
address: Memory address (preferably function entry point)
|
||||
comment: Comment text (empty string removes comment)
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
# Try setting as function comment first
|
||||
payload = {"comment": comment}
|
||||
response = self.safe_patch(port, f"functions/{address}", payload)
|
||||
if response.get("success", False):
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Log why function comment failed before falling back
|
||||
error = response.get("error", {})
|
||||
logger.debug(
|
||||
"Function comment at %s failed (%s), falling back to pre-comment",
|
||||
address,
|
||||
error.get("code", "UNKNOWN"),
|
||||
)
|
||||
|
||||
# Fallback to pre-comment
|
||||
return self.comments_set(
|
||||
address=address,
|
||||
comment=comment,
|
||||
comment_type="pre",
|
||||
port=port,
|
||||
)
|
||||
285
src/mcghidra/mixins/base.py
Normal file
285
src/mcghidra/mixins/base.py
Normal file
@ -0,0 +1,285 @@
|
||||
"""Base mixin class for MCGhidra domain mixins.
|
||||
|
||||
Provides shared state and utilities for all domain mixins.
|
||||
"""
|
||||
|
||||
import time
|
||||
from threading import RLock
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import MCPMixin
|
||||
|
||||
from ..config import get_config
|
||||
from ..core.http_client import (
|
||||
safe_delete,
|
||||
safe_get,
|
||||
safe_patch,
|
||||
safe_post,
|
||||
safe_put,
|
||||
simplify_response,
|
||||
)
|
||||
from ..core.logging import log_debug, log_error, log_info, log_warning
|
||||
from ..core.pagination import paginate_response
|
||||
|
||||
|
||||
class MCGhidraMixinBase(MCPMixin):
|
||||
"""Base class for MCGhidra domain mixins.
|
||||
|
||||
Provides shared instance state and common utilities.
|
||||
All domain mixins should inherit from this class.
|
||||
"""
|
||||
|
||||
# Shared state across all mixins
|
||||
_instances: Dict[int, Dict[str, Any]] = {}
|
||||
_instances_lock = RLock()
|
||||
_current_port: Optional[int] = None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the mixin with shared state."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_current_port(cls) -> Optional[int]:
|
||||
"""Get the current working instance port."""
|
||||
return cls._current_port
|
||||
|
||||
@classmethod
|
||||
def set_current_port(cls, port: int) -> None:
|
||||
"""Set the current working instance port."""
|
||||
cls._current_port = port
|
||||
|
||||
@classmethod
|
||||
def get_instance_port(cls, port: Optional[int] = None) -> int:
|
||||
"""Get instance port, using current if not specified.
|
||||
|
||||
Args:
|
||||
port: Explicit port (optional)
|
||||
|
||||
Returns:
|
||||
Port number to use
|
||||
|
||||
Raises:
|
||||
ValueError: If no port specified and no current instance set
|
||||
"""
|
||||
if port is not None:
|
||||
return port
|
||||
if cls._current_port is not None:
|
||||
return cls._current_port
|
||||
config = get_config()
|
||||
# Try default port
|
||||
default_port = config.quick_discovery_range.start
|
||||
if default_port in cls._instances:
|
||||
return default_port
|
||||
raise ValueError(
|
||||
"No Ghidra instance specified. Use instances_use(port) to set a working instance, "
|
||||
"or pass port= parameter explicitly."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_instance(cls, port: int, url: Optional[str] = None) -> str:
|
||||
"""Register a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
url: Optional URL override
|
||||
|
||||
Returns:
|
||||
Status message
|
||||
"""
|
||||
config = get_config()
|
||||
if url is None:
|
||||
url = f"http://{config.ghidra_host}:{port}"
|
||||
|
||||
# Verify instance is responsive
|
||||
try:
|
||||
response = safe_get(port, "")
|
||||
if not response.get("success", False):
|
||||
return f"Failed to connect to Ghidra instance on port {port}"
|
||||
|
||||
# Check API version
|
||||
api_version = response.get("api_version", 0)
|
||||
if api_version < config.expected_api_version:
|
||||
return (
|
||||
f"API version mismatch: got {api_version}, "
|
||||
f"expected {config.expected_api_version}"
|
||||
)
|
||||
|
||||
with cls._instances_lock:
|
||||
cls._instances[port] = {
|
||||
"url": url,
|
||||
"project": response.get("project", ""),
|
||||
"file": response.get("file", ""),
|
||||
"registered_at": time.time(),
|
||||
}
|
||||
|
||||
return f"Registered Ghidra instance on port {port}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Error registering instance: {e}"
|
||||
|
||||
@classmethod
|
||||
def unregister_instance(cls, port: int) -> str:
|
||||
"""Unregister a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Status message
|
||||
"""
|
||||
with cls._instances_lock:
|
||||
if port in cls._instances:
|
||||
del cls._instances[port]
|
||||
if cls._current_port == port:
|
||||
cls._current_port = None
|
||||
return f"Unregistered Ghidra instance on port {port}"
|
||||
return f"No instance registered on port {port}"
|
||||
|
||||
@classmethod
|
||||
def list_instances(cls) -> Dict[int, Dict[str, Any]]:
|
||||
"""Get all registered instances.
|
||||
|
||||
Returns:
|
||||
Dict mapping port to instance info
|
||||
"""
|
||||
with cls._instances_lock:
|
||||
return dict(cls._instances)
|
||||
|
||||
@classmethod
|
||||
def get_instance_info(cls, port: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get info for a specific instance.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Instance info dict or None
|
||||
"""
|
||||
with cls._instances_lock:
|
||||
return cls._instances.get(port)
|
||||
|
||||
def _get_session_id(self, ctx: Optional[Context]) -> str:
|
||||
"""Extract session ID from FastMCP context.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context
|
||||
|
||||
Returns:
|
||||
Session identifier string
|
||||
"""
|
||||
if ctx is None:
|
||||
return "default"
|
||||
|
||||
# Try various context attributes
|
||||
if hasattr(ctx, "session") and ctx.session:
|
||||
return str(ctx.session)
|
||||
if hasattr(ctx, "client_id") and ctx.client_id:
|
||||
return str(ctx.client_id)
|
||||
if hasattr(ctx, "request_id") and ctx.request_id:
|
||||
return f"req-{ctx.request_id}"
|
||||
|
||||
return "default"
|
||||
|
||||
# Convenience methods for subclasses
|
||||
def safe_get(
|
||||
self, port: int, endpoint: str, params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Make GET request to Ghidra instance."""
|
||||
return safe_get(port, endpoint, params)
|
||||
|
||||
def safe_post(self, port: int, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||
"""Make POST request to Ghidra instance."""
|
||||
return safe_post(port, endpoint, data)
|
||||
|
||||
def safe_put(
|
||||
self, port: int, endpoint: str, data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Make PUT request to Ghidra instance."""
|
||||
return safe_put(port, endpoint, data)
|
||||
|
||||
def safe_patch(
|
||||
self, port: int, endpoint: str, data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Make PATCH request to Ghidra instance."""
|
||||
return safe_patch(port, endpoint, data)
|
||||
|
||||
def safe_delete(self, port: int, endpoint: str) -> Dict[str, Any]:
|
||||
"""Make DELETE request to Ghidra instance."""
|
||||
return safe_delete(port, endpoint)
|
||||
|
||||
def simplify_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Simplify HATEOAS response."""
|
||||
return simplify_response(response)
|
||||
|
||||
def paginate_response(
|
||||
self,
|
||||
data: list,
|
||||
query_params: Dict,
|
||||
tool_name: str,
|
||||
session_id: str = "default",
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[list] = None,
|
||||
) -> Dict:
|
||||
"""Create paginated response with optional field projection."""
|
||||
return paginate_response(
|
||||
data=data,
|
||||
query_params=query_params,
|
||||
tool_name=tool_name,
|
||||
session_id=session_id,
|
||||
page_size=page_size,
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
def filtered_paginate(
|
||||
self,
|
||||
data: list,
|
||||
query_params: Dict,
|
||||
tool_name: str,
|
||||
session_id: str = "default",
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[list] = None,
|
||||
) -> Dict:
|
||||
"""Paginate with field projection and budget guard.
|
||||
|
||||
Convenience wrapper that applies field projection then delegates
|
||||
to paginate_response. Prefer this over paginate_response for any
|
||||
tool that could return large result sets.
|
||||
"""
|
||||
return self.paginate_response(
|
||||
data=data,
|
||||
query_params=query_params,
|
||||
tool_name=tool_name,
|
||||
session_id=session_id,
|
||||
page_size=page_size,
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Async logging helpers
|
||||
async def log_info(self, ctx: Optional[Context], message: str) -> None:
|
||||
"""Log info message."""
|
||||
await log_info(ctx, message)
|
||||
|
||||
async def log_debug(self, ctx: Optional[Context], message: str) -> None:
|
||||
"""Log debug message."""
|
||||
await log_debug(ctx, message)
|
||||
|
||||
async def log_warning(self, ctx: Optional[Context], message: str) -> None:
|
||||
"""Log warning message."""
|
||||
await log_warning(ctx, message)
|
||||
|
||||
async def log_error(self, ctx: Optional[Context], message: str) -> None:
|
||||
"""Log error message."""
|
||||
await log_error(ctx, message)
|
||||
171
src/mcghidra/mixins/bookmarks.py
Normal file
171
src/mcghidra/mixins/bookmarks.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""Bookmarks mixin for MCGhidra.
|
||||
|
||||
Provides tools for managing Ghidra bookmarks (annotations at addresses).
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class BookmarksMixin(MCGhidraMixinBase):
|
||||
"""Mixin for bookmark operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing bookmarks with type/category filtering
|
||||
- Creating bookmarks at addresses
|
||||
- Deleting bookmarks
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def bookmarks_list(
|
||||
self,
|
||||
type: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List bookmarks with optional type/category filtering.
|
||||
|
||||
Args:
|
||||
type: Filter by bookmark type (e.g. "Note", "Warning", "Error", "Info")
|
||||
category: Filter by bookmark category
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Bookmarks per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter bookmark comments
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all bookmarks without pagination
|
||||
fields: Field names to keep (e.g. ['address', 'type', 'comment']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of bookmarks
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("bookmarks", 1000)
|
||||
params: Dict[str, Any] = {"limit": cap}
|
||||
if type:
|
||||
params["type"] = type
|
||||
if category:
|
||||
params["category"] = category
|
||||
|
||||
response = self.safe_get(port, "bookmarks", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
bookmarks = simplified.get("result", [])
|
||||
if not isinstance(bookmarks, list):
|
||||
bookmarks = []
|
||||
|
||||
query_params = {
|
||||
"tool": "bookmarks_list",
|
||||
"port": port,
|
||||
"type": type,
|
||||
"category": category,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=bookmarks,
|
||||
query_params=query_params,
|
||||
tool_name="bookmarks_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def bookmarks_create(
|
||||
self,
|
||||
address: str,
|
||||
type: str = "Note",
|
||||
category: str = "",
|
||||
comment: str = "",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a bookmark at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
type: Bookmark type (default: "Note"). Common types: Note, Warning, Error, Info
|
||||
category: Bookmark category (optional grouping string)
|
||||
comment: Bookmark comment text
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created bookmark information
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {
|
||||
"address": address,
|
||||
"type": type,
|
||||
"category": category,
|
||||
"comment": comment,
|
||||
}
|
||||
response = self.safe_post(port, "bookmarks", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def bookmarks_delete(
|
||||
self,
|
||||
address: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete all bookmarks at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
response = self.safe_delete(port, f"bookmarks/{address}")
|
||||
return self.simplify_response(response)
|
||||
174
src/mcghidra/mixins/cursors.py
Normal file
174
src/mcghidra/mixins/cursors.py
Normal file
@ -0,0 +1,174 @@
|
||||
"""Cursor management mixin for MCGhidra.
|
||||
|
||||
Provides tools for managing pagination cursors.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
|
||||
from ..core.pagination import get_cursor_manager
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class CursorsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for cursor management.
|
||||
|
||||
Provides tools for navigating paginated results.
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def cursor_next(
|
||||
self, cursor_id: str, ctx: Optional[Context] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the next page of results for a cursor.
|
||||
|
||||
Args:
|
||||
cursor_id: The cursor identifier from a previous paginated response
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Next page of results with updated pagination info
|
||||
"""
|
||||
session_id = self._get_session_id(ctx)
|
||||
cursor_manager = get_cursor_manager()
|
||||
|
||||
# Get and advance cursor
|
||||
state = cursor_manager.get_cursor(cursor_id, session_id)
|
||||
if not state:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "CURSOR_NOT_FOUND",
|
||||
"message": f"Cursor '{cursor_id}' not found or expired. "
|
||||
"Cursors expire after 5 minutes of inactivity.",
|
||||
},
|
||||
}
|
||||
|
||||
# Advance to next page
|
||||
state = cursor_manager.advance_cursor(cursor_id, session_id)
|
||||
if not state:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "CURSOR_ADVANCE_FAILED",
|
||||
"message": "Failed to advance cursor",
|
||||
},
|
||||
}
|
||||
|
||||
current_page = cursor_manager.get_page(state)
|
||||
response_cursor = cursor_id if state.has_more else None
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"result": current_page,
|
||||
"pagination": {
|
||||
"cursor_id": response_cursor,
|
||||
"session_id": session_id,
|
||||
"total_count": state.total_count,
|
||||
"filtered_count": state.filtered_count,
|
||||
"page_size": state.page_size,
|
||||
"current_page": state.current_page,
|
||||
"total_pages": state.total_pages,
|
||||
"has_more": state.has_more,
|
||||
"grep_pattern": state.grep_pattern,
|
||||
"items_returned": len(current_page),
|
||||
},
|
||||
}
|
||||
|
||||
if state.has_more:
|
||||
remaining = state.filtered_count - (state.current_page * state.page_size)
|
||||
response["_message"] = (
|
||||
f"Showing {len(current_page)} of {state.filtered_count} items "
|
||||
f"(page {state.current_page}/{state.total_pages}). "
|
||||
f"To get the next {min(state.page_size, remaining)} items, call: "
|
||||
f"cursor_next(cursor_id='{cursor_id}')"
|
||||
)
|
||||
else:
|
||||
response["_message"] = (
|
||||
f"Complete: {len(current_page)} items returned (final page)"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@mcp_tool()
|
||||
def cursor_list(
|
||||
self, ctx: Optional[Context] = None, all_sessions: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""List active cursors for the current session.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (auto-injected)
|
||||
all_sessions: Include cursors from all sessions (admin only)
|
||||
|
||||
Returns:
|
||||
List of active cursors with their status
|
||||
"""
|
||||
session_id = self._get_session_id(ctx)
|
||||
cursor_manager = get_cursor_manager()
|
||||
|
||||
if all_sessions:
|
||||
cursors = cursor_manager.list_cursors()
|
||||
else:
|
||||
cursors = cursor_manager.list_cursors(session_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"cursors": cursors,
|
||||
"session_id": session_id,
|
||||
"count": len(cursors),
|
||||
}
|
||||
|
||||
@mcp_tool()
|
||||
def cursor_delete(
|
||||
self, cursor_id: str, ctx: Optional[Context] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a specific cursor.
|
||||
|
||||
Args:
|
||||
cursor_id: The cursor identifier to delete
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Confirmation of deletion
|
||||
"""
|
||||
session_id = self._get_session_id(ctx)
|
||||
cursor_manager = get_cursor_manager()
|
||||
|
||||
deleted = cursor_manager.delete_cursor(cursor_id, session_id)
|
||||
|
||||
if deleted:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Cursor '{cursor_id}' deleted",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "CURSOR_NOT_FOUND",
|
||||
"message": f"Cursor '{cursor_id}' not found or belongs to another session",
|
||||
},
|
||||
}
|
||||
|
||||
@mcp_tool()
|
||||
def cursor_delete_all(self, ctx: Optional[Context] = None) -> Dict[str, Any]:
|
||||
"""Delete all cursors for the current session.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Number of cursors deleted
|
||||
"""
|
||||
session_id = self._get_session_id(ctx)
|
||||
cursor_manager = get_cursor_manager()
|
||||
|
||||
count = cursor_manager.delete_session_cursors(session_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Deleted {count} cursor(s) for session",
|
||||
"deleted_count": count,
|
||||
}
|
||||
390
src/mcghidra/mixins/data.py
Normal file
390
src/mcghidra/mixins/data.py
Normal file
@ -0,0 +1,390 @@
|
||||
"""Data mixin for MCGhidra.
|
||||
|
||||
Provides tools for data items and strings operations.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class DataMixin(MCGhidraMixinBase):
|
||||
"""Mixin for data operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing and searching data items
|
||||
- Creating and modifying data
|
||||
- Working with strings
|
||||
- Setting data types
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def data_list(
|
||||
self,
|
||||
addr: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
name_contains: Optional[str] = None,
|
||||
type: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List defined data items with filtering and cursor-based pagination.
|
||||
|
||||
Args:
|
||||
addr: Filter by address (hexadecimal)
|
||||
name: Exact name match filter (case-sensitive)
|
||||
name_contains: Substring name filter (case-insensitive)
|
||||
type: Filter by data type (e.g. "string", "dword")
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Items per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter results
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all results without pagination
|
||||
fields: Field names to keep (e.g. ['address', 'name']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Data items with pagination metadata
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
params = {"offset": 0, "limit": 10000}
|
||||
if addr:
|
||||
params["addr"] = addr
|
||||
if name:
|
||||
params["name"] = name
|
||||
if name_contains:
|
||||
params["name_contains"] = name_contains
|
||||
if type:
|
||||
params["type"] = type
|
||||
|
||||
response = self.safe_get(port, "data", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
all_data = simplified.get("result", [])
|
||||
if not isinstance(all_data, list):
|
||||
all_data = []
|
||||
|
||||
query_params = {
|
||||
"tool": "data_list",
|
||||
"port": port,
|
||||
"addr": addr,
|
||||
"name": name,
|
||||
"name_contains": name_contains,
|
||||
"type": type,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=all_data,
|
||||
query_params=query_params,
|
||||
tool_name="data_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def data_list_strings(
|
||||
self,
|
||||
filter: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all defined strings in the binary with pagination.
|
||||
|
||||
Args:
|
||||
filter: Server-side string content filter
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Items per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter results (e.g., "password|key")
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all strings without pagination
|
||||
fields: Field names to keep (e.g. ['value', 'address']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
List of strings with pagination info
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
fetch_limit = 10000 if return_all else max(page_size * 10, 2000)
|
||||
|
||||
params = {"offset": 0, "limit": fetch_limit}
|
||||
if filter:
|
||||
params["filter"] = filter
|
||||
|
||||
response = self.safe_get(port, "strings", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result_data = simplified.get("result", [])
|
||||
if not isinstance(result_data, list):
|
||||
return simplified
|
||||
|
||||
query_params = {
|
||||
"tool": "data_list_strings",
|
||||
"port": port,
|
||||
"filter": filter,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=result_data,
|
||||
query_params=query_params,
|
||||
tool_name="data_list_strings",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def data_create(
|
||||
self,
|
||||
address: str,
|
||||
data_type: str,
|
||||
size: Optional[int] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Define a new data item at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
data_type: Data type (e.g. "string", "dword", "byte")
|
||||
size: Optional size in bytes
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result with created data information
|
||||
"""
|
||||
if not address or not data_type:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address and data_type parameters are required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"address": address, "type": data_type}
|
||||
if size is not None:
|
||||
payload["size"] = size
|
||||
|
||||
response = self.safe_post(port, "data", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def data_rename(
|
||||
self,
|
||||
address: str,
|
||||
name: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Rename a data item.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
name: New name for the data item
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address or not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address and name parameters are required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"address": address, "newName": name}
|
||||
response = self.safe_post(port, "data", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def data_delete(
|
||||
self,
|
||||
address: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete data at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"address": address, "action": "delete"}
|
||||
response = self.safe_post(port, "data/delete", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def data_set_type(
|
||||
self,
|
||||
address: str,
|
||||
data_type: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Set the data type of a data item.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
data_type: Data type name (e.g. "uint32_t", "char[10]")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address or not data_type:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address and data_type parameters are required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"address": address, "type": data_type}
|
||||
response = self.safe_post(port, "data/type", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/strings")
|
||||
def resource_strings_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List strings (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of strings (capped at 1000)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("strings", 1000)
|
||||
|
||||
response = self.safe_get(port, "strings", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
strings = simplified.get("result", [])
|
||||
if not isinstance(strings, list):
|
||||
strings = []
|
||||
|
||||
return {
|
||||
"strings": strings[:cap],
|
||||
"count": len(strings),
|
||||
"capped_at": cap if len(strings) >= cap else None,
|
||||
"_hint": "Use data_list_strings() tool for full pagination" if len(strings) >= cap else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/data")
|
||||
def resource_data_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List data items (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of data items (capped at 1000)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("data", 1000)
|
||||
|
||||
response = self.safe_get(port, "data", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
data_items = simplified.get("result", [])
|
||||
if not isinstance(data_items, list):
|
||||
data_items = []
|
||||
|
||||
return {
|
||||
"data": data_items[:cap],
|
||||
"count": len(data_items),
|
||||
"capped_at": cap if len(data_items) >= cap else None,
|
||||
"_hint": "Use data_list() tool for full pagination" if len(data_items) >= cap else None,
|
||||
}
|
||||
217
src/mcghidra/mixins/datatypes.py
Normal file
217
src/mcghidra/mixins/datatypes.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Data types mixin for MCGhidra.
|
||||
|
||||
Provides tools for managing enum and typedef data types.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class DataTypesMixin(MCGhidraMixinBase):
|
||||
"""Mixin for enum and typedef data type operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing and creating enum data types
|
||||
- Listing and creating typedef data types
|
||||
"""
|
||||
|
||||
# --- Enums ---
|
||||
|
||||
@mcp_tool()
|
||||
def enums_list(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List enum data types with their members.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Enums per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter enum names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all enums without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'size', 'members']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of enum data types
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("enums", 500)
|
||||
response = self.safe_get(port, "datatypes/enums", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
enums = simplified.get("result", [])
|
||||
if not isinstance(enums, list):
|
||||
enums = []
|
||||
|
||||
query_params = {"tool": "enums_list", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=enums,
|
||||
query_params=query_params,
|
||||
tool_name="enums_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def enums_create(
|
||||
self,
|
||||
name: str,
|
||||
size: int = 4,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new enum data type.
|
||||
|
||||
Args:
|
||||
name: Name for the new enum
|
||||
size: Size in bytes (default: 4)
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created enum information
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": name, "size": size}
|
||||
response = self.safe_post(port, "datatypes/enums", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
# --- Typedefs ---
|
||||
|
||||
@mcp_tool()
|
||||
def typedefs_list(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List typedef data types.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Typedefs per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter typedef names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all typedefs without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'base_type']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of typedef data types
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("typedefs", 500)
|
||||
response = self.safe_get(port, "datatypes/typedefs", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
typedefs = simplified.get("result", [])
|
||||
if not isinstance(typedefs, list):
|
||||
typedefs = []
|
||||
|
||||
query_params = {"tool": "typedefs_list", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=typedefs,
|
||||
query_params=query_params,
|
||||
tool_name="typedefs_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def typedefs_create(
|
||||
self,
|
||||
name: str,
|
||||
base_type: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new typedef data type.
|
||||
|
||||
Args:
|
||||
name: Name for the new typedef
|
||||
base_type: Name of the base data type (e.g. "int", "uint32_t", "char*")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created typedef information
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "name parameter is required",
|
||||
},
|
||||
}
|
||||
if not base_type:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "base_type parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": name, "base_type": base_type}
|
||||
response = self.safe_post(port, "datatypes/typedefs", payload)
|
||||
return self.simplify_response(response)
|
||||
1235
src/mcghidra/mixins/docker.py
Normal file
1235
src/mcghidra/mixins/docker.py
Normal file
File diff suppressed because it is too large
Load Diff
554
src/mcghidra/mixins/functions.py
Normal file
554
src/mcghidra/mixins/functions.py
Normal file
@ -0,0 +1,554 @@
|
||||
"""Functions mixin for MCGhidra.
|
||||
|
||||
Provides tools for function analysis, decompilation, and manipulation.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class FunctionsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for function operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing and searching functions
|
||||
- Decompiling functions
|
||||
- Disassembling functions
|
||||
- Renaming functions
|
||||
- Setting function signatures
|
||||
- Managing function variables
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def functions_list(
|
||||
self,
|
||||
name_contains: Optional[str] = None,
|
||||
name_regex: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List functions with cursor-based pagination and server-side filtering.
|
||||
|
||||
Args:
|
||||
name_contains: Server-side substring filter on function name (faster than grep for large binaries)
|
||||
name_regex: Server-side regex filter on function name
|
||||
address: Filter by exact function address (hex)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Functions per page (default: 50, max: 500)
|
||||
grep: Client-side regex pattern to filter function names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all functions without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'address']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of functions
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
params = {"limit": 10000}
|
||||
if name_contains:
|
||||
params["name_contains"] = name_contains
|
||||
if name_regex:
|
||||
params["name_matches_regex"] = name_regex
|
||||
if address:
|
||||
params["addr"] = address
|
||||
|
||||
response = self.safe_get(port, "functions", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
functions = simplified.get("result", [])
|
||||
if not isinstance(functions, list):
|
||||
functions = []
|
||||
|
||||
query_params = {
|
||||
"tool": "functions_list",
|
||||
"port": port,
|
||||
"name_contains": name_contains,
|
||||
"name_regex": name_regex,
|
||||
"address": address,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=functions,
|
||||
query_params=query_params,
|
||||
tool_name="functions_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_get(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get detailed information about a function.
|
||||
|
||||
Args:
|
||||
name: Function name (mutually exclusive with address)
|
||||
address: Function address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Detailed function information
|
||||
"""
|
||||
if not name and not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either name or address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
if address:
|
||||
endpoint = f"functions/{address}"
|
||||
else:
|
||||
endpoint = f"functions/by-name/{quote(name)}"
|
||||
|
||||
response = self.safe_get(port, endpoint)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_decompile(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
syntax_tree: bool = False,
|
||||
style: str = "normalize",
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get decompiled code for a function with line pagination.
|
||||
|
||||
Args:
|
||||
name: Function name (mutually exclusive with address)
|
||||
address: Function address in hex format
|
||||
syntax_tree: Include syntax tree (default: False)
|
||||
style: Decompiler style (default: "normalize")
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Lines per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter lines
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all lines without pagination
|
||||
fields: Field names to keep (for structured results)
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Decompiled code with pagination
|
||||
"""
|
||||
if not name and not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either name or address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
if address:
|
||||
endpoint = f"functions/{address}/decompile"
|
||||
else:
|
||||
endpoint = f"functions/by-name/{quote(name)}/decompile"
|
||||
|
||||
params = {"syntaxTree": str(syntax_tree).lower(), "style": style}
|
||||
response = self.safe_get(port, endpoint, params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result = simplified.get("result", {})
|
||||
decompiled = result.get("decompiled_text", result.get("ccode", ""))
|
||||
|
||||
if not decompiled:
|
||||
return simplified
|
||||
|
||||
# Split into lines for pagination
|
||||
lines = decompiled.split("\n")
|
||||
|
||||
query_params = {
|
||||
"tool": "functions_decompile",
|
||||
"port": port,
|
||||
"name": name,
|
||||
"address": address,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
paginated = self.filtered_paginate(
|
||||
data=lines,
|
||||
query_params=query_params,
|
||||
tool_name="functions_decompile",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Convert lines back to text in result (skip if guarded)
|
||||
if paginated.get("success") and not paginated.get("guarded"):
|
||||
paginated["result"] = "\n".join(paginated.get("result", []))
|
||||
paginated["function_name"] = result.get("name", name or address)
|
||||
|
||||
return paginated
|
||||
|
||||
@mcp_tool()
|
||||
def functions_disassemble(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get disassembly for a function with instruction pagination.
|
||||
|
||||
Args:
|
||||
name: Function name (mutually exclusive with address)
|
||||
address: Function address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Instructions per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter instructions
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all instructions without pagination
|
||||
fields: Field names to keep (for structured results)
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Disassembly with pagination
|
||||
"""
|
||||
if not name and not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either name or address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
if address:
|
||||
endpoint = f"functions/{address}/disassembly"
|
||||
else:
|
||||
endpoint = f"functions/by-name/{quote(name)}/disassembly"
|
||||
|
||||
response = self.safe_get(port, endpoint)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result = simplified.get("result", {})
|
||||
disasm_text = result.get("disassembly_text", "")
|
||||
|
||||
if not disasm_text:
|
||||
return simplified
|
||||
|
||||
# Split into lines for pagination
|
||||
lines = [line for line in disasm_text.split("\n") if line.strip()]
|
||||
|
||||
query_params = {
|
||||
"tool": "functions_disassemble",
|
||||
"port": port,
|
||||
"name": name,
|
||||
"address": address,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
paginated = self.filtered_paginate(
|
||||
data=lines,
|
||||
query_params=query_params,
|
||||
tool_name="functions_disassemble",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Convert lines back to text (skip if guarded)
|
||||
if paginated.get("success") and not paginated.get("guarded"):
|
||||
paginated["result"] = "\n".join(paginated.get("result", []))
|
||||
paginated["function_name"] = result.get("name", name or address)
|
||||
|
||||
return paginated
|
||||
|
||||
@mcp_tool()
|
||||
def functions_rename(
|
||||
self,
|
||||
old_name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
new_name: str = "",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Rename a function.
|
||||
|
||||
Args:
|
||||
old_name: Current function name
|
||||
address: Function address in hex format
|
||||
new_name: New name for the function
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not new_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "new_name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
if not old_name and not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either old_name or address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
if address:
|
||||
endpoint = f"functions/{address}"
|
||||
else:
|
||||
endpoint = f"functions/by-name/{quote(old_name)}"
|
||||
|
||||
payload = {"name": new_name}
|
||||
response = self.safe_patch(port, endpoint, payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_create(self, address: str, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Create a function at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created function information
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"address": address}
|
||||
response = self.safe_post(port, "functions", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_set_signature(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
signature: str = "",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Set the signature/prototype of a function.
|
||||
|
||||
Args:
|
||||
name: Function name
|
||||
address: Function address in hex format
|
||||
signature: New function signature (e.g., "int foo(char* arg1, int arg2)")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not signature:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "signature parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
if not name and not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either name or address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
if address:
|
||||
endpoint = f"functions/{address}/signature"
|
||||
else:
|
||||
endpoint = f"functions/by-name/{quote(name)}/signature"
|
||||
|
||||
payload = {"signature": signature}
|
||||
response = self.safe_put(port, endpoint, payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/functions")
|
||||
def resource_functions_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List functions (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of functions (capped at 1000)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("functions", 1000)
|
||||
|
||||
response = self.safe_get(port, "functions", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
functions = simplified.get("result", [])
|
||||
if not isinstance(functions, list):
|
||||
functions = []
|
||||
|
||||
return {
|
||||
"functions": functions[:cap],
|
||||
"count": len(functions),
|
||||
"capped_at": cap if len(functions) >= cap else None,
|
||||
"_hint": "Use functions_list() tool for full pagination" if len(functions) >= cap else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/function/decompile/address/{address}")
|
||||
def resource_decompiled_by_address(
|
||||
self, port: Optional[int] = None, address: Optional[str] = None
|
||||
) -> str:
|
||||
"""MCP Resource: Get decompiled code by address.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
address: Function address
|
||||
|
||||
Returns:
|
||||
Decompiled code as text
|
||||
"""
|
||||
if not address:
|
||||
return "Error: address is required"
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
response = self.safe_get(port, f"functions/{address}/decompile")
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
error = simplified.get("error", {})
|
||||
return f"Error: {error.get('message', 'Unknown error')}"
|
||||
|
||||
result = simplified.get("result", {})
|
||||
return result.get("decompiled_text", result.get("ccode", "No decompiled code available"))
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/function/decompile/name/{name}")
|
||||
def resource_decompiled_by_name(
|
||||
self, port: Optional[int] = None, name: Optional[str] = None
|
||||
) -> str:
|
||||
"""MCP Resource: Get decompiled code by function name.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
name: Function name
|
||||
|
||||
Returns:
|
||||
Decompiled code as text
|
||||
"""
|
||||
if not name:
|
||||
return "Error: name is required"
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
response = self.safe_get(port, f"functions/by-name/{quote(name)}/decompile")
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
error = simplified.get("error", {})
|
||||
return f"Error: {error.get('message', 'Unknown error')}"
|
||||
|
||||
result = simplified.get("result", {})
|
||||
return result.get("decompiled_text", result.get("ccode", "No decompiled code available"))
|
||||
342
src/mcghidra/mixins/instances.py
Normal file
342
src/mcghidra/mixins/instances.py
Normal file
@ -0,0 +1,342 @@
|
||||
"""Instance management mixin for MCGhidra.
|
||||
|
||||
Provides tools for discovering, registering, and managing Ghidra instances.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class InstancesMixin(MCGhidraMixinBase):
|
||||
"""Mixin for Ghidra instance management.
|
||||
|
||||
Provides tools for:
|
||||
- Discovering Ghidra instances
|
||||
- Registering/unregistering instances
|
||||
- Setting current working instance
|
||||
- Listing available instances
|
||||
"""
|
||||
|
||||
def _discover_instances(
|
||||
self,
|
||||
port_range: range,
|
||||
host: Optional[str] = None,
|
||||
timeout: float = 0.5,
|
||||
) -> Dict[int, Dict[str, Any]]:
|
||||
"""Discover Ghidra instances by scanning ports.
|
||||
|
||||
Args:
|
||||
port_range: Range of ports to scan
|
||||
host: Host to scan (defaults to config)
|
||||
timeout: Connection timeout per port
|
||||
|
||||
Returns:
|
||||
Dict of discovered instances
|
||||
"""
|
||||
import requests
|
||||
config = get_config()
|
||||
if host is None:
|
||||
host = config.ghidra_host
|
||||
|
||||
discovered = {}
|
||||
|
||||
for port in port_range:
|
||||
try:
|
||||
url = f"http://{host}:{port}/"
|
||||
response = requests.get(url, timeout=timeout)
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
# Verify it's a Ghidra HATEOAS API
|
||||
if "_links" in data or "api_version" in data:
|
||||
instance_info = {
|
||||
"url": f"http://{host}:{port}",
|
||||
"project": data.get("project", ""),
|
||||
"file": data.get("file", ""),
|
||||
"api_version": data.get("api_version"),
|
||||
"discovered_at": time.time(),
|
||||
}
|
||||
discovered[port] = instance_info
|
||||
|
||||
# Also register it
|
||||
with self._instances_lock:
|
||||
self._instances[port] = instance_info
|
||||
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return discovered
|
||||
|
||||
@mcp_tool()
|
||||
def instances_list(self) -> Dict[str, Any]:
|
||||
"""List all active Ghidra instances.
|
||||
|
||||
This is the primary tool for working with instances. It automatically
|
||||
discovers new instances on the default host before listing.
|
||||
|
||||
Returns:
|
||||
Dict containing 'instances' list with all available Ghidra instances
|
||||
"""
|
||||
config = get_config()
|
||||
# Auto-discover before listing
|
||||
self._discover_instances(config.quick_discovery_range, timeout=0.5)
|
||||
|
||||
with self._instances_lock:
|
||||
return {
|
||||
"instances": [
|
||||
{
|
||||
"port": port,
|
||||
"url": info["url"],
|
||||
"project": info.get("project", ""),
|
||||
"file": info.get("file", ""),
|
||||
}
|
||||
for port, info in self._instances.items()
|
||||
]
|
||||
}
|
||||
|
||||
@mcp_tool()
|
||||
def instances_discover(self, host: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Discover Ghidra instances on a specific host.
|
||||
|
||||
Use this ONLY when you need to discover instances on a different host.
|
||||
For normal usage, just use instances_list() which auto-discovers.
|
||||
|
||||
Args:
|
||||
host: Host to scan for Ghidra instances (default: configured host)
|
||||
|
||||
Returns:
|
||||
Dict containing 'instances' list with all available instances
|
||||
"""
|
||||
config = get_config()
|
||||
self._discover_instances(config.full_discovery_range, host=host, timeout=0.5)
|
||||
|
||||
with self._instances_lock:
|
||||
return {
|
||||
"instances": [
|
||||
{
|
||||
"port": port,
|
||||
"url": info["url"],
|
||||
"project": info.get("project", ""),
|
||||
"file": info.get("file", ""),
|
||||
}
|
||||
for port, info in self._instances.items()
|
||||
]
|
||||
}
|
||||
|
||||
@mcp_tool()
|
||||
def instances_register(self, port: int, url: Optional[str] = None) -> str:
|
||||
"""Register a new Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Port number of the Ghidra instance
|
||||
url: Optional URL if different from default
|
||||
|
||||
Returns:
|
||||
Confirmation message or error
|
||||
"""
|
||||
return self.register_instance(port, url)
|
||||
|
||||
@mcp_tool()
|
||||
def instances_unregister(self, port: int) -> str:
|
||||
"""Unregister a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Port number of the instance to unregister
|
||||
|
||||
Returns:
|
||||
Confirmation message
|
||||
"""
|
||||
return self.unregister_instance(port)
|
||||
|
||||
@mcp_tool()
|
||||
def instances_use(self, port: int) -> str:
|
||||
"""Set the current working Ghidra instance.
|
||||
|
||||
All subsequent commands will use this instance by default.
|
||||
|
||||
Args:
|
||||
port: Port number of the instance to use
|
||||
|
||||
Returns:
|
||||
Confirmation message with instance details
|
||||
"""
|
||||
# Register lazily without blocking HTTP calls.
|
||||
# If the instance is unknown, create a stub entry — the first
|
||||
# actual tool call (functions_list, etc.) will validate the
|
||||
# connection and fail fast with a clear error if unreachable.
|
||||
with self._instances_lock:
|
||||
if port not in self._instances:
|
||||
config = get_config()
|
||||
self._instances[port] = {
|
||||
"url": f"http://{config.ghidra_host}:{port}",
|
||||
"project": "",
|
||||
"file": "",
|
||||
"registered_at": time.time(),
|
||||
"lazy": True,
|
||||
}
|
||||
|
||||
self.set_current_port(port)
|
||||
|
||||
info = self.get_instance_info(port)
|
||||
if info:
|
||||
return (
|
||||
f"Now using Ghidra instance on port {port}\n"
|
||||
f"Project: {info.get('project', 'N/A')}\n"
|
||||
f"File: {info.get('file', 'N/A')}"
|
||||
)
|
||||
return f"Now using Ghidra instance on port {port}"
|
||||
|
||||
@mcp_tool()
|
||||
def instances_current(self) -> Dict[str, Any]:
|
||||
"""Get information about the current working instance.
|
||||
|
||||
Returns:
|
||||
Dict with current instance information or error message
|
||||
"""
|
||||
port = self.get_current_port()
|
||||
if port is None:
|
||||
return {
|
||||
"error": "No current instance set. Use instances_use(port) first.",
|
||||
"available_instances": list(self._instances.keys()),
|
||||
}
|
||||
|
||||
info = self.get_instance_info(port)
|
||||
if info:
|
||||
return {
|
||||
"port": port,
|
||||
"url": info["url"],
|
||||
"project": info.get("project", ""),
|
||||
"file": info.get("file", ""),
|
||||
}
|
||||
|
||||
return {"port": port, "status": "registered but no details available"}
|
||||
|
||||
@mcp_tool()
|
||||
def program_info(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Get full program metadata (architecture, language, compiler, image base, memory size).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Program metadata including architecture, language, compiler spec,
|
||||
image base address, and total memory size
|
||||
"""
|
||||
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, "program")
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_resource(uri="ghidra://instances")
|
||||
def resource_instances_list(self) -> Dict[str, Any]:
|
||||
"""MCP Resource: List all active Ghidra instances.
|
||||
|
||||
Returns a lightweight enumeration of instances for quick reference.
|
||||
"""
|
||||
config = get_config()
|
||||
# Auto-discover before listing
|
||||
self._discover_instances(config.quick_discovery_range, timeout=0.3)
|
||||
|
||||
with self._instances_lock:
|
||||
instances = [
|
||||
{
|
||||
"port": port,
|
||||
"project": info.get("project", ""),
|
||||
"file": info.get("file", ""),
|
||||
}
|
||||
for port, info in self._instances.items()
|
||||
]
|
||||
|
||||
return {
|
||||
"instances": instances,
|
||||
"count": len(instances),
|
||||
"current_port": self.get_current_port(),
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}")
|
||||
def resource_instance_info(self, port: int) -> Dict[str, Any]:
|
||||
"""MCP Resource: Get detailed information about a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Port number of the instance
|
||||
|
||||
Returns:
|
||||
Instance information including program details
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
response = self.safe_get(port, "")
|
||||
if not response.get("success", True):
|
||||
return response
|
||||
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/summary")
|
||||
def resource_program_summary(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: Get a summary of the program loaded in a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional, uses current if not specified)
|
||||
|
||||
Returns:
|
||||
Program summary with statistics
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# Get program info
|
||||
response = self.safe_get(port, "")
|
||||
if not response.get("success", True):
|
||||
return response
|
||||
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
# Get function count
|
||||
funcs_response = self.safe_get(port, "functions", {"limit": 1})
|
||||
func_count = funcs_response.get("size", 0) if funcs_response.get("success", True) else 0
|
||||
|
||||
# Get string count
|
||||
strings_response = self.safe_get(port, "data/strings", {"limit": 1})
|
||||
string_count = strings_response.get("size", 0) if strings_response.get("success", True) else 0
|
||||
|
||||
return {
|
||||
"program_name": simplified.get("result", {}).get("program_name", simplified.get("file", "")),
|
||||
"language": simplified.get("result", {}).get("language", ""),
|
||||
"processor": simplified.get("result", {}).get("processor", ""),
|
||||
"format": simplified.get("result", {}).get("format", ""),
|
||||
"function_count": func_count,
|
||||
"string_count": string_count,
|
||||
"port": port,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/program")
|
||||
def resource_program_info(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: Get program metadata for a Ghidra instance.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
Program metadata (architecture, language, compiler, image base)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
response = self.safe_get(port, "program")
|
||||
return self.simplify_response(response)
|
||||
129
src/mcghidra/mixins/memory.py
Normal file
129
src/mcghidra/mixins/memory.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""Memory mixin for MCGhidra.
|
||||
|
||||
Provides tools for memory read/write operations.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastmcp.contrib.mcp_mixin import mcp_tool
|
||||
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class MemoryMixin(MCGhidraMixinBase):
|
||||
"""Mixin for memory operations.
|
||||
|
||||
Provides tools for:
|
||||
- Reading memory bytes
|
||||
- Writing memory bytes (use with caution)
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def memory_read(
|
||||
self,
|
||||
address: str,
|
||||
length: int = 16,
|
||||
format: str = "hex",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Read bytes from memory.
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
length: Number of bytes to read (default: 16)
|
||||
format: Output format - "hex", "base64", or "string" (default: "hex")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Memory contents in the requested format
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
params = {
|
||||
"address": address,
|
||||
"length": length,
|
||||
"format": format,
|
||||
}
|
||||
|
||||
response = self.safe_get(port, "memory", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if "result" in simplified and isinstance(simplified["result"], dict):
|
||||
result = simplified["result"]
|
||||
memory_info = {
|
||||
"success": True,
|
||||
"address": result.get("address", address),
|
||||
"length": result.get("bytesRead", length),
|
||||
"format": format,
|
||||
}
|
||||
|
||||
if "hexBytes" in result:
|
||||
memory_info["hexBytes"] = result["hexBytes"]
|
||||
if "rawBytes" in result:
|
||||
memory_info["rawBytes"] = result["rawBytes"]
|
||||
|
||||
return memory_info
|
||||
|
||||
return simplified
|
||||
|
||||
@mcp_tool()
|
||||
def memory_write(
|
||||
self,
|
||||
address: str,
|
||||
bytes_data: str,
|
||||
format: str = "hex",
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Write bytes to memory (use with caution).
|
||||
|
||||
Args:
|
||||
address: Memory address in hex format
|
||||
bytes_data: Data to write (format depends on 'format' parameter)
|
||||
format: Input format - "hex", "base64", or "string" (default: "hex")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
if not bytes_data:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Bytes parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {
|
||||
"bytes": bytes_data,
|
||||
"format": format,
|
||||
}
|
||||
|
||||
response = self.safe_patch(port, f"programs/current/memory/{address}", payload)
|
||||
return self.simplify_response(response)
|
||||
211
src/mcghidra/mixins/namespaces.py
Normal file
211
src/mcghidra/mixins/namespaces.py
Normal file
@ -0,0 +1,211 @@
|
||||
"""Namespaces mixin for MCGhidra.
|
||||
|
||||
Provides tools for querying namespaces and class definitions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class NamespacesMixin(MCGhidraMixinBase):
|
||||
"""Mixin for namespace and class operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing all non-global namespaces
|
||||
- Listing class namespaces with qualified names
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def namespaces_list(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all non-global namespaces with pagination.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Namespaces per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter namespace names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all namespaces without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'id']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of namespaces
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("namespaces", 500)
|
||||
response = self.safe_get(port, "namespaces", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
namespaces = simplified.get("result", [])
|
||||
if not isinstance(namespaces, list):
|
||||
namespaces = []
|
||||
|
||||
query_params = {"tool": "namespaces_list", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=namespaces,
|
||||
query_params=query_params,
|
||||
tool_name="namespaces_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def classes_list(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List class namespaces with qualified names.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Classes per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter class names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all classes without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'qualified_name']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of class namespaces
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("classes", 500)
|
||||
response = self.safe_get(port, "classes", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
classes = simplified.get("result", [])
|
||||
if not isinstance(classes, list):
|
||||
classes = []
|
||||
|
||||
query_params = {"tool": "classes_list", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=classes,
|
||||
query_params=query_params,
|
||||
tool_name="classes_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/namespaces")
|
||||
def resource_namespaces_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List namespaces (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of namespaces (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("namespaces", 500)
|
||||
|
||||
response = self.safe_get(port, "namespaces", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
namespaces = simplified.get("result", [])
|
||||
if not isinstance(namespaces, list):
|
||||
namespaces = []
|
||||
|
||||
return {
|
||||
"namespaces": namespaces[:cap],
|
||||
"count": len(namespaces),
|
||||
"capped_at": cap if len(namespaces) >= cap else None,
|
||||
"_hint": "Use namespaces_list() tool for full pagination"
|
||||
if len(namespaces) >= cap
|
||||
else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/classes")
|
||||
def resource_classes_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List classes (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of class namespaces (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("classes", 500)
|
||||
|
||||
response = self.safe_get(port, "classes", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
classes = simplified.get("result", [])
|
||||
if not isinstance(classes, list):
|
||||
classes = []
|
||||
|
||||
return {
|
||||
"classes": classes[:cap],
|
||||
"count": len(classes),
|
||||
"capped_at": cap if len(classes) >= cap else None,
|
||||
"_hint": "Use classes_list() tool for full pagination"
|
||||
if len(classes) >= cap
|
||||
else None,
|
||||
}
|
||||
122
src/mcghidra/mixins/segments.py
Normal file
122
src/mcghidra/mixins/segments.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Segments mixin for MCGhidra.
|
||||
|
||||
Provides tools for querying memory segments (sections) and their permissions.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class SegmentsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for memory segment operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing memory segments with permissions and size info
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def segments_list(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List memory segments with R/W/X permissions and size info.
|
||||
|
||||
Args:
|
||||
name: Filter by segment name (server-side, exact match)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Segments per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter segment names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all segments without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'start', 'permissions']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of memory segments
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("segments", 500)
|
||||
params = {"limit": cap}
|
||||
if name:
|
||||
params["name"] = name
|
||||
|
||||
response = self.safe_get(port, "segments", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
segments = simplified.get("result", [])
|
||||
if not isinstance(segments, list):
|
||||
segments = []
|
||||
|
||||
query_params = {"tool": "segments_list", "port": port, "name": name, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=segments,
|
||||
query_params=query_params,
|
||||
tool_name="segments_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/segments")
|
||||
def resource_segments_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List memory segments (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of memory segments (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("segments", 500)
|
||||
|
||||
response = self.safe_get(port, "segments", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
segments = simplified.get("result", [])
|
||||
if not isinstance(segments, list):
|
||||
segments = []
|
||||
|
||||
return {
|
||||
"segments": segments[:cap],
|
||||
"count": len(segments),
|
||||
"capped_at": cap if len(segments) >= cap else None,
|
||||
"_hint": "Use segments_list() tool for full pagination"
|
||||
if len(segments) >= cap
|
||||
else None,
|
||||
}
|
||||
425
src/mcghidra/mixins/structs.py
Normal file
425
src/mcghidra/mixins/structs.py
Normal file
@ -0,0 +1,425 @@
|
||||
"""Structs mixin for MCGhidra.
|
||||
|
||||
Provides tools for struct data type operations.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class StructsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for struct operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing and searching structs
|
||||
- Getting struct details with field pagination
|
||||
- Creating and modifying structs
|
||||
- Managing struct fields
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def structs_list(
|
||||
self,
|
||||
category: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all struct data types with cursor-based pagination.
|
||||
|
||||
Args:
|
||||
category: Filter by category path (e.g. "/winapi")
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Items per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter struct names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all results without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'size']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Structs with pagination metadata
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
params = {"offset": 0, "limit": 10000}
|
||||
if category:
|
||||
params["category"] = category
|
||||
|
||||
response = self.safe_get(port, "structs", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
all_structs = simplified.get("result", [])
|
||||
if not isinstance(all_structs, list):
|
||||
all_structs = []
|
||||
|
||||
query_params = {
|
||||
"tool": "structs_list",
|
||||
"port": port,
|
||||
"category": category,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=all_structs,
|
||||
query_params=query_params,
|
||||
tool_name="structs_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def structs_get(
|
||||
self,
|
||||
name: str,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get detailed information about a struct with field pagination.
|
||||
|
||||
Args:
|
||||
name: Struct name
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Fields per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter fields
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all fields without pagination
|
||||
fields: Field names to keep per struct field item. Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Struct details with paginated fields
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Struct name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
params = {"name": name}
|
||||
response = self.safe_get(port, "structs", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
result = simplified.get("result", simplified)
|
||||
|
||||
# Extract struct info and fields
|
||||
struct_info = {}
|
||||
struct_fields = []
|
||||
|
||||
if isinstance(result, dict):
|
||||
for key, value in result.items():
|
||||
if key == "fields" and isinstance(value, list):
|
||||
struct_fields = value
|
||||
else:
|
||||
struct_info[key] = value
|
||||
|
||||
# If few fields and no grep, return as-is
|
||||
if len(struct_fields) <= 10 and not grep:
|
||||
return simplified
|
||||
|
||||
query_params = {
|
||||
"tool": "structs_get",
|
||||
"port": port,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
# Paginate fields
|
||||
paginated = self.filtered_paginate(
|
||||
data=struct_fields,
|
||||
query_params=query_params,
|
||||
tool_name="structs_get",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Merge struct metadata with paginated fields (skip if guarded)
|
||||
if paginated.get("success") and not paginated.get("guarded"):
|
||||
paginated["struct_name"] = struct_info.get("name", name)
|
||||
paginated["struct_size"] = struct_info.get("size", struct_info.get("length"))
|
||||
paginated["struct_category"] = struct_info.get("category", struct_info.get("categoryPath"))
|
||||
paginated["struct_description"] = struct_info.get("description")
|
||||
paginated["fields"] = paginated.pop("result", [])
|
||||
|
||||
if "_message" in paginated:
|
||||
paginated["_message"] = paginated["_message"].replace("items", "fields")
|
||||
|
||||
return paginated
|
||||
|
||||
@mcp_tool()
|
||||
def structs_create(
|
||||
self,
|
||||
name: str,
|
||||
category: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new struct data type.
|
||||
|
||||
Args:
|
||||
name: Name for the new struct
|
||||
category: Category path (e.g. "/custom")
|
||||
description: Optional description
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created struct information
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Struct name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": name}
|
||||
if category:
|
||||
payload["category"] = category
|
||||
if description:
|
||||
payload["description"] = description
|
||||
|
||||
response = self.safe_post(port, "structs/create", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def structs_add_field(
|
||||
self,
|
||||
struct_name: str,
|
||||
field_name: str,
|
||||
field_type: str,
|
||||
offset: Optional[int] = None,
|
||||
comment: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Add a field to an existing struct.
|
||||
|
||||
Args:
|
||||
struct_name: Name of the struct to modify
|
||||
field_name: Name for the new field
|
||||
field_type: Data type for the field
|
||||
offset: Specific offset (appends if not specified)
|
||||
comment: Optional field comment
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not struct_name or not field_name or not field_type:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "struct_name, field_name, and field_type are required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {
|
||||
"struct": struct_name,
|
||||
"fieldName": field_name,
|
||||
"fieldType": field_type,
|
||||
}
|
||||
if offset is not None:
|
||||
payload["offset"] = offset
|
||||
if comment:
|
||||
payload["comment"] = comment
|
||||
|
||||
response = self.safe_post(port, "structs/addfield", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def structs_update_field(
|
||||
self,
|
||||
struct_name: str,
|
||||
field_name: Optional[str] = None,
|
||||
field_offset: Optional[int] = None,
|
||||
new_name: Optional[str] = None,
|
||||
new_type: Optional[str] = None,
|
||||
new_comment: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an existing field in a struct.
|
||||
|
||||
Args:
|
||||
struct_name: Name of the struct to modify
|
||||
field_name: Name of the field to update (OR field_offset)
|
||||
field_offset: Offset of the field to update (OR field_name)
|
||||
new_name: New name for the field
|
||||
new_type: New data type for the field
|
||||
new_comment: New comment for the field
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not struct_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "struct_name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
if not field_name and field_offset is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either field_name or field_offset must be provided",
|
||||
},
|
||||
}
|
||||
|
||||
if not new_name and not new_type and new_comment is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "At least one of new_name, new_type, or new_comment required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"struct": struct_name}
|
||||
if field_name:
|
||||
payload["fieldName"] = field_name
|
||||
if field_offset is not None:
|
||||
payload["fieldOffset"] = field_offset
|
||||
if new_name:
|
||||
payload["newName"] = new_name
|
||||
if new_type:
|
||||
payload["newType"] = new_type
|
||||
if new_comment is not None:
|
||||
payload["newComment"] = new_comment
|
||||
|
||||
response = self.safe_post(port, "structs/updatefield", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def structs_delete(
|
||||
self,
|
||||
name: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a struct data type.
|
||||
|
||||
Args:
|
||||
name: Name of the struct to delete
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Struct name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": name}
|
||||
response = self.safe_post(port, "structs/delete", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/structs")
|
||||
def resource_structs_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List structs (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of structs (capped at 1000)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("structs", 1000)
|
||||
|
||||
response = self.safe_get(port, "structs", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
structs = simplified.get("result", [])
|
||||
if not isinstance(structs, list):
|
||||
structs = []
|
||||
|
||||
return {
|
||||
"structs": structs[:cap],
|
||||
"count": len(structs),
|
||||
"capped_at": cap if len(structs) >= cap else None,
|
||||
"_hint": "Use structs_list() tool for full pagination" if len(structs) >= cap else None,
|
||||
}
|
||||
422
src/mcghidra/mixins/symbols.py
Normal file
422
src/mcghidra/mixins/symbols.py
Normal file
@ -0,0 +1,422 @@
|
||||
"""Symbols mixin for MCGhidra.
|
||||
|
||||
Provides tools for symbol table operations including labels, imports, and exports.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class SymbolsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for symbol table operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing all symbols with pagination
|
||||
- Querying imported symbols (external references)
|
||||
- Querying exported symbols (entry points)
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_list(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List symbols with cursor-based pagination.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Symbols per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter symbol names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all symbols without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'address']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of symbols
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
response = self.safe_get(port, "symbols", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
symbols = simplified.get("result", [])
|
||||
if not isinstance(symbols, list):
|
||||
symbols = []
|
||||
|
||||
query_params = {"tool": "symbols_list", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=symbols,
|
||||
query_params=query_params,
|
||||
tool_name="symbols_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_imports(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List imported symbols (external references) with pagination.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Imports per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter import names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all imports without pagination
|
||||
fields: Field names to keep. Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of imported symbols
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
response = self.safe_get(port, "symbols/imports", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
imports = simplified.get("result", [])
|
||||
if not isinstance(imports, list):
|
||||
imports = []
|
||||
|
||||
query_params = {"tool": "symbols_imports", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=imports,
|
||||
query_params=query_params,
|
||||
tool_name="symbols_imports",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_exports(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List exported symbols (entry points) with pagination.
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Exports per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter export names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all exports without pagination
|
||||
fields: Field names to keep. Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of exported symbols
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
response = self.safe_get(port, "symbols/exports", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
exports = simplified.get("result", [])
|
||||
if not isinstance(exports, list):
|
||||
exports = []
|
||||
|
||||
query_params = {"tool": "symbols_exports", "port": port, "grep": grep}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=exports,
|
||||
query_params=query_params,
|
||||
tool_name="symbols_exports",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_create(
|
||||
self,
|
||||
name: str,
|
||||
address: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new label/symbol at the specified address.
|
||||
|
||||
Args:
|
||||
name: Name for the new symbol
|
||||
address: Memory address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Created symbol information
|
||||
"""
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "name parameter is required",
|
||||
},
|
||||
}
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": name, "address": address}
|
||||
response = self.safe_post(port, "symbols", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_rename(
|
||||
self,
|
||||
address: str,
|
||||
new_name: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Rename the primary symbol at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address of the symbol in hex format
|
||||
new_name: New name for the symbol
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result with old and new names
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "address parameter is required",
|
||||
},
|
||||
}
|
||||
if not new_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "new_name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
payload = {"name": new_name}
|
||||
response = self.safe_patch(port, f"symbols/{address}", payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
@mcp_tool()
|
||||
def symbols_delete(
|
||||
self,
|
||||
address: str,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete the primary symbol at the specified address.
|
||||
|
||||
Args:
|
||||
address: Memory address of the symbol in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
response = self.safe_delete(port, f"symbols/{address}")
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/symbols")
|
||||
def resource_symbols_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List symbols (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of symbols (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
|
||||
response = self.safe_get(port, "symbols", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
symbols = simplified.get("result", [])
|
||||
if not isinstance(symbols, list):
|
||||
symbols = []
|
||||
|
||||
return {
|
||||
"symbols": symbols[:cap],
|
||||
"count": len(symbols),
|
||||
"capped_at": cap if len(symbols) >= cap else None,
|
||||
"_hint": "Use symbols_list() tool for full pagination" if len(symbols) >= cap else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/symbols/imports")
|
||||
def resource_symbols_imports(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List imported symbols (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of imported symbols (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
|
||||
response = self.safe_get(port, "symbols/imports", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
imports = simplified.get("result", [])
|
||||
if not isinstance(imports, list):
|
||||
imports = []
|
||||
|
||||
return {
|
||||
"imports": imports[:cap],
|
||||
"count": len(imports),
|
||||
"capped_at": cap if len(imports) >= cap else None,
|
||||
"_hint": "Use symbols_imports() tool for full pagination"
|
||||
if len(imports) >= cap
|
||||
else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/symbols/exports")
|
||||
def resource_symbols_exports(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List exported symbols (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of exported symbols (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("symbols", 1000)
|
||||
|
||||
response = self.safe_get(port, "symbols/exports", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
exports = simplified.get("result", [])
|
||||
if not isinstance(exports, list):
|
||||
exports = []
|
||||
|
||||
return {
|
||||
"exports": exports[:cap],
|
||||
"count": len(exports),
|
||||
"capped_at": cap if len(exports) >= cap else None,
|
||||
"_hint": "Use symbols_exports() tool for full pagination"
|
||||
if len(exports) >= cap
|
||||
else None,
|
||||
}
|
||||
261
src/mcghidra/mixins/variables.py
Normal file
261
src/mcghidra/mixins/variables.py
Normal file
@ -0,0 +1,261 @@
|
||||
"""Variables mixin for MCGhidra.
|
||||
|
||||
Provides tools for querying global and function-local variables.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class VariablesMixin(MCGhidraMixinBase):
|
||||
"""Mixin for variable operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing global and function variables
|
||||
- Querying local variables and parameters for a specific function
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def variables_list(
|
||||
self,
|
||||
global_only: bool = False,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List variables with cursor-based pagination.
|
||||
|
||||
Args:
|
||||
global_only: Only return global variables (default: False)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Variables per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter variable names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all variables without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'type', 'address']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of variables
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("variables", 1000)
|
||||
params = {"limit": cap}
|
||||
if global_only:
|
||||
params["global_only"] = "true"
|
||||
|
||||
response = self.safe_get(port, "variables", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
variables = simplified.get("result", [])
|
||||
if not isinstance(variables, list):
|
||||
variables = []
|
||||
|
||||
query_params = {
|
||||
"tool": "variables_list",
|
||||
"port": port,
|
||||
"global_only": global_only,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=variables,
|
||||
query_params=query_params,
|
||||
tool_name="variables_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def functions_variables(
|
||||
self,
|
||||
address: str,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List local variables and parameters for a specific function.
|
||||
|
||||
Args:
|
||||
address: Function address in hex format
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Variables per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter variable names
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all variables without pagination
|
||||
fields: Field names to keep (e.g. ['name', 'type', 'storage']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Paginated list of function variables
|
||||
"""
|
||||
if not address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Address parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
response = self.safe_get(port, f"functions/{address}/variables")
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
variables = simplified.get("result", [])
|
||||
if not isinstance(variables, list):
|
||||
variables = []
|
||||
|
||||
query_params = {
|
||||
"tool": "functions_variables",
|
||||
"port": port,
|
||||
"address": address,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=variables,
|
||||
query_params=query_params,
|
||||
tool_name="functions_variables",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
@mcp_tool()
|
||||
def variables_rename(
|
||||
self,
|
||||
function_address: str,
|
||||
variable_name: str,
|
||||
new_name: str,
|
||||
new_type: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Rename a variable (and optionally retype) within a function.
|
||||
|
||||
Args:
|
||||
function_address: Function address in hex format
|
||||
variable_name: Current name of the variable
|
||||
new_name: New name for the variable
|
||||
new_type: New data type (optional, e.g. "int", "char*")
|
||||
port: Ghidra instance port (optional)
|
||||
|
||||
Returns:
|
||||
Operation result
|
||||
"""
|
||||
if not function_address:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "function_address parameter is required",
|
||||
},
|
||||
}
|
||||
if not variable_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "variable_name parameter is required",
|
||||
},
|
||||
}
|
||||
if not new_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "new_name parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
payload: dict = {"name": new_name}
|
||||
if new_type:
|
||||
payload["data_type"] = new_type
|
||||
|
||||
endpoint = f"functions/{function_address}/variables/{quote(variable_name)}"
|
||||
response = self.safe_patch(port, endpoint, payload)
|
||||
return self.simplify_response(response)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/variables")
|
||||
def resource_variables_list(self, port: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""MCP Resource: List variables (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
|
||||
Returns:
|
||||
List of variables (capped)
|
||||
"""
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("variables", 1000)
|
||||
|
||||
response = self.safe_get(port, "variables", {"limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
variables = simplified.get("result", [])
|
||||
if not isinstance(variables, list):
|
||||
variables = []
|
||||
|
||||
return {
|
||||
"variables": variables[:cap],
|
||||
"count": len(variables),
|
||||
"capped_at": cap if len(variables) >= cap else None,
|
||||
"_hint": "Use variables_list() tool for full pagination"
|
||||
if len(variables) >= cap
|
||||
else None,
|
||||
}
|
||||
199
src/mcghidra/mixins/xrefs.py
Normal file
199
src/mcghidra/mixins/xrefs.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""Cross-references mixin for MCGhidra.
|
||||
|
||||
Provides tools for cross-reference (xref) operations.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp.contrib.mcp_mixin import mcp_resource, mcp_tool
|
||||
|
||||
from ..config import get_config
|
||||
from .base import MCGhidraMixinBase
|
||||
|
||||
|
||||
class XrefsMixin(MCGhidraMixinBase):
|
||||
"""Mixin for cross-reference operations.
|
||||
|
||||
Provides tools for:
|
||||
- Listing references to an address
|
||||
- Listing references from an address
|
||||
- Filtering by reference type
|
||||
"""
|
||||
|
||||
@mcp_tool()
|
||||
def xrefs_list(
|
||||
self,
|
||||
to_addr: Optional[str] = None,
|
||||
from_addr: Optional[str] = None,
|
||||
type: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
page_size: int = 50,
|
||||
grep: Optional[str] = None,
|
||||
grep_ignorecase: bool = True,
|
||||
return_all: bool = False,
|
||||
fields: Optional[List[str]] = None,
|
||||
ctx: Optional[Context] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List cross-references with filtering and pagination.
|
||||
|
||||
Args:
|
||||
to_addr: Filter references to this address (hex)
|
||||
from_addr: Filter references from this address (hex)
|
||||
type: Filter by reference type ("CALL", "READ", "WRITE", etc.)
|
||||
port: Ghidra instance port (optional)
|
||||
page_size: Items per page (default: 50, max: 500)
|
||||
grep: Regex pattern to filter results
|
||||
grep_ignorecase: Case-insensitive grep (default: True)
|
||||
return_all: Return all results without pagination
|
||||
fields: Field names to keep (e.g. ['fromAddress', 'toAddress']). Reduces response size.
|
||||
ctx: FastMCP context (auto-injected)
|
||||
|
||||
Returns:
|
||||
Cross-references with pagination metadata
|
||||
"""
|
||||
if not to_addr and not from_addr:
|
||||
return {
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "MISSING_PARAMETER",
|
||||
"message": "Either to_addr or from_addr parameter is required",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": {"code": "NO_INSTANCE", "message": str(e)}}
|
||||
|
||||
config = get_config()
|
||||
|
||||
params = {"offset": 0, "limit": 10000}
|
||||
if to_addr:
|
||||
params["to_addr"] = to_addr
|
||||
if from_addr:
|
||||
params["from_addr"] = from_addr
|
||||
if type:
|
||||
params["type"] = type
|
||||
|
||||
response = self.safe_get(port, "xrefs", params)
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
all_xrefs = simplified.get("result", [])
|
||||
if not isinstance(all_xrefs, list):
|
||||
all_xrefs = []
|
||||
|
||||
query_params = {
|
||||
"tool": "xrefs_list",
|
||||
"port": port,
|
||||
"to_addr": to_addr,
|
||||
"from_addr": from_addr,
|
||||
"type": type,
|
||||
"grep": grep,
|
||||
}
|
||||
session_id = self._get_session_id(ctx)
|
||||
|
||||
return self.filtered_paginate(
|
||||
data=all_xrefs,
|
||||
query_params=query_params,
|
||||
tool_name="xrefs_list",
|
||||
session_id=session_id,
|
||||
page_size=min(page_size, config.max_page_size),
|
||||
grep=grep,
|
||||
grep_ignorecase=grep_ignorecase,
|
||||
return_all=return_all,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
# Resources
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/xrefs/to/{address}")
|
||||
def resource_xrefs_to(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
address: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""MCP Resource: Get references to an address (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
address: Target address
|
||||
|
||||
Returns:
|
||||
References to the address (capped at 1000)
|
||||
"""
|
||||
if not address:
|
||||
return {"error": "Address is required"}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("xrefs", 1000)
|
||||
|
||||
response = self.safe_get(port, "xrefs", {"to_addr": address, "limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
xrefs = simplified.get("result", [])
|
||||
if not isinstance(xrefs, list):
|
||||
xrefs = []
|
||||
|
||||
return {
|
||||
"address": address,
|
||||
"xrefs_to": xrefs[:cap],
|
||||
"count": len(xrefs),
|
||||
"capped_at": cap if len(xrefs) >= cap else None,
|
||||
"_hint": "Use xrefs_list(to_addr=...) for full pagination" if len(xrefs) >= cap else None,
|
||||
}
|
||||
|
||||
@mcp_resource(uri="ghidra://instance/{port}/xrefs/from/{address}")
|
||||
def resource_xrefs_from(
|
||||
self,
|
||||
port: Optional[int] = None,
|
||||
address: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""MCP Resource: Get references from an address (capped).
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port
|
||||
address: Source address
|
||||
|
||||
Returns:
|
||||
References from the address (capped at 1000)
|
||||
"""
|
||||
if not address:
|
||||
return {"error": "Address is required"}
|
||||
|
||||
try:
|
||||
port = self.get_instance_port(port)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
config = get_config()
|
||||
cap = config.resource_caps.get("xrefs", 1000)
|
||||
|
||||
response = self.safe_get(port, "xrefs", {"from_addr": address, "limit": cap})
|
||||
simplified = self.simplify_response(response)
|
||||
|
||||
if not simplified.get("success", True):
|
||||
return simplified
|
||||
|
||||
xrefs = simplified.get("result", [])
|
||||
if not isinstance(xrefs, list):
|
||||
xrefs = []
|
||||
|
||||
return {
|
||||
"address": address,
|
||||
"xrefs_from": xrefs[:cap],
|
||||
"count": len(xrefs),
|
||||
"capped_at": cap if len(xrefs) >= cap else None,
|
||||
"_hint": "Use xrefs_list(from_addr=...) for full pagination" if len(xrefs) >= cap else None,
|
||||
}
|
||||
236
src/mcghidra/server.py
Normal file
236
src/mcghidra/server.py
Normal file
@ -0,0 +1,236 @@
|
||||
"""MCGhidra Server - FastMCP server composing all mixins.
|
||||
|
||||
This module creates and configures the FastMCP server by composing
|
||||
all domain-specific mixins into a single MCP server.
|
||||
"""
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from .config import MCGhidraConfig, get_config, set_config
|
||||
from .core.logging import configure_logging
|
||||
from .mixins import (
|
||||
AnalysisMixin,
|
||||
BookmarksMixin,
|
||||
CursorsMixin,
|
||||
DataMixin,
|
||||
DataTypesMixin,
|
||||
DockerMixin,
|
||||
FunctionsMixin,
|
||||
InstancesMixin,
|
||||
MemoryMixin,
|
||||
NamespacesMixin,
|
||||
SegmentsMixin,
|
||||
StructsMixin,
|
||||
SymbolsMixin,
|
||||
VariablesMixin,
|
||||
XrefsMixin,
|
||||
)
|
||||
|
||||
|
||||
def create_server(
|
||||
name: str = "MCGhidra",
|
||||
config: Optional[MCGhidraConfig] = None,
|
||||
) -> FastMCP:
|
||||
"""Create and configure the MCGhidra server.
|
||||
|
||||
Args:
|
||||
name: Server name
|
||||
config: Optional configuration override
|
||||
|
||||
Returns:
|
||||
Configured FastMCP server instance
|
||||
"""
|
||||
if config:
|
||||
set_config(config)
|
||||
|
||||
# Create the FastMCP server
|
||||
mcp = FastMCP(name)
|
||||
|
||||
# Instantiate all mixins
|
||||
instances_mixin = InstancesMixin()
|
||||
functions_mixin = FunctionsMixin()
|
||||
data_mixin = DataMixin()
|
||||
structs_mixin = StructsMixin()
|
||||
analysis_mixin = AnalysisMixin()
|
||||
memory_mixin = MemoryMixin()
|
||||
xrefs_mixin = XrefsMixin()
|
||||
cursors_mixin = CursorsMixin()
|
||||
docker_mixin = DockerMixin()
|
||||
symbols_mixin = SymbolsMixin()
|
||||
segments_mixin = SegmentsMixin()
|
||||
variables_mixin = VariablesMixin()
|
||||
namespaces_mixin = NamespacesMixin()
|
||||
bookmarks_mixin = BookmarksMixin()
|
||||
datatypes_mixin = DataTypesMixin()
|
||||
|
||||
# Register all mixins with the server
|
||||
# Each mixin registers its tools, resources, and prompts
|
||||
instances_mixin.register_all(mcp)
|
||||
functions_mixin.register_all(mcp)
|
||||
data_mixin.register_all(mcp)
|
||||
structs_mixin.register_all(mcp)
|
||||
analysis_mixin.register_all(mcp)
|
||||
memory_mixin.register_all(mcp)
|
||||
xrefs_mixin.register_all(mcp)
|
||||
cursors_mixin.register_all(mcp)
|
||||
docker_mixin.register_all(mcp)
|
||||
symbols_mixin.register_all(mcp)
|
||||
segments_mixin.register_all(mcp)
|
||||
variables_mixin.register_all(mcp)
|
||||
namespaces_mixin.register_all(mcp)
|
||||
bookmarks_mixin.register_all(mcp)
|
||||
datatypes_mixin.register_all(mcp)
|
||||
|
||||
# Optional feedback collection
|
||||
cfg = get_config()
|
||||
if cfg.feedback_enabled:
|
||||
try:
|
||||
from fastmcp_feedback import add_feedback_tools
|
||||
|
||||
db_path = Path(cfg.feedback_db_path)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
add_feedback_tools(mcp, database_url=f"sqlite:///{db_path}")
|
||||
except ImportError:
|
||||
pass # fastmcp-feedback not installed — skip silently
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
def _periodic_discovery(interval: int = 30):
|
||||
"""Background thread for periodic instance discovery.
|
||||
|
||||
Uses a short timeout per port so a full scan completes quickly
|
||||
even when most ports are unreachable.
|
||||
|
||||
Args:
|
||||
interval: Seconds between discovery attempts
|
||||
"""
|
||||
import requests as _requests
|
||||
|
||||
from .mixins.base import MCGhidraMixinBase
|
||||
|
||||
config = get_config()
|
||||
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
try:
|
||||
# Quick scan — use discovery_timeout (0.5s), NOT request_timeout (30s)
|
||||
for port in config.quick_discovery_range:
|
||||
try:
|
||||
url = f"http://{config.ghidra_host}:{port}/"
|
||||
resp = _requests.get(
|
||||
url,
|
||||
timeout=config.discovery_timeout,
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if resp.ok:
|
||||
response = resp.json()
|
||||
if response.get("success", False):
|
||||
with MCGhidraMixinBase._instances_lock:
|
||||
if port not in MCGhidraMixinBase._instances:
|
||||
MCGhidraMixinBase._instances[port] = {
|
||||
"url": url.rstrip("/"),
|
||||
"project": response.get("project", ""),
|
||||
"file": response.get("file", ""),
|
||||
"discovered_at": time.time(),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _handle_sigint(signum, frame):
|
||||
"""Handle SIGINT gracefully."""
|
||||
print("\nShutting down MCGhidra...", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the MCGhidra server."""
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Configure logging early (DEBUG if MCGHIDRAMCP_DEBUG is set)
|
||||
log_level = logging.DEBUG if os.environ.get("MCGHIDRAMCP_DEBUG") else logging.INFO
|
||||
configure_logging(log_level)
|
||||
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
package_version = version("mcghidra")
|
||||
except Exception:
|
||||
package_version = "2025.12.1"
|
||||
|
||||
print(f"🔬 MCGhidra v{package_version}", file=sys.stderr)
|
||||
print(" Reverse engineering bridge for Ghidra", file=sys.stderr)
|
||||
|
||||
# Check Docker availability
|
||||
docker_available = shutil.which("docker") is not None
|
||||
if docker_available:
|
||||
print(" 🐳 Docker available (use docker_* tools for container management)", file=sys.stderr)
|
||||
else:
|
||||
print(" ⚠ Docker not found (container management disabled)", file=sys.stderr)
|
||||
|
||||
config = get_config()
|
||||
|
||||
if config.feedback_enabled:
|
||||
print(f" 📋 Feedback collection: {config.feedback_db_path}", file=sys.stderr)
|
||||
|
||||
# Create and configure the server
|
||||
mcp = create_server()
|
||||
|
||||
# Initial instance discovery
|
||||
print(f" Discovering Ghidra instances on {config.ghidra_host}...", file=sys.stderr)
|
||||
|
||||
from .core.http_client import safe_get
|
||||
from .mixins.base import MCGhidraMixinBase
|
||||
|
||||
found = 0
|
||||
for port in config.quick_discovery_range:
|
||||
try:
|
||||
response = safe_get(port, "")
|
||||
if response.get("success", False):
|
||||
with MCGhidraMixinBase._instances_lock:
|
||||
MCGhidraMixinBase._instances[port] = {
|
||||
"url": f"http://{config.ghidra_host}:{port}",
|
||||
"project": response.get("project", ""),
|
||||
"file": response.get("file", ""),
|
||||
"discovered_at": time.time(),
|
||||
}
|
||||
found += 1
|
||||
print(f" ✓ Found instance on port {port}", file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if found == 0:
|
||||
print(" ⚠ No Ghidra instances found (they can be discovered later)", file=sys.stderr)
|
||||
else:
|
||||
print(f" Found {found} Ghidra instance(s)", file=sys.stderr)
|
||||
|
||||
# Start background discovery thread
|
||||
discovery_thread = threading.Thread(
|
||||
target=_periodic_discovery,
|
||||
daemon=True,
|
||||
name="MCGhidra-Discovery",
|
||||
)
|
||||
discovery_thread.start()
|
||||
|
||||
# Set up signal handler
|
||||
signal.signal(signal.SIGINT, _handle_sigint)
|
||||
|
||||
print(" Starting MCP server...", file=sys.stderr)
|
||||
|
||||
# Run the server
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
102
test_comments.py
102
test_comments.py
@ -1,6 +1,10 @@
|
||||
#!/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
|
||||
different types of comments in Ghidra, including plate, pre, post, EOL,
|
||||
repeatable, and decompiler comments.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
@ -13,21 +17,23 @@ import requests
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("comment_test")
|
||||
|
||||
# Direct HTTP test functions
|
||||
def test_http_api_comments(port=8192, address="08000200"):
|
||||
"""Test setting comments directly with HTTP API"""
|
||||
"""
|
||||
Test setting and retrieving comments using direct HTTP API.
|
||||
|
||||
Args:
|
||||
port: Ghidra HTTP API port
|
||||
address: Memory address for comments
|
||||
"""
|
||||
logger.info("===== Testing HTTP API Comments =====")
|
||||
base_url = f"http://localhost:{port}"
|
||||
|
||||
# Test each comment type
|
||||
comment_types = ["plate", "pre", "post", "eol", "repeatable"]
|
||||
|
||||
for i, comment_type in enumerate(comment_types):
|
||||
# Set comment
|
||||
for comment_type in comment_types:
|
||||
comment_text = f"TEST {comment_type.upper()} COMMENT {int(time.time())}"
|
||||
logger.info(f"Setting {comment_type} comment: {comment_text}")
|
||||
|
||||
@ -40,37 +46,42 @@ def test_http_api_comments(port=8192, address="08000200"):
|
||||
logger.info(f"Response: {r.text}")
|
||||
|
||||
if r.status_code == 200:
|
||||
# Get the comment back to verify
|
||||
r_get = requests.get(url, timeout=10)
|
||||
logger.info(f"GET Status code: {r_get.status_code}")
|
||||
logger.info(f"GET Response: {r_get.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting {comment_type} comment: {e}")
|
||||
|
||||
# MCP Bridge test functions
|
||||
async def test_bridge_comments():
|
||||
"""Test the bridge comment functionality"""
|
||||
"""
|
||||
Test MCP bridge comment functionality.
|
||||
|
||||
Sets and clears both plate comments and decompiler comments using the
|
||||
MCP bridge interface.
|
||||
"""
|
||||
logger.info("===== Testing MCP Bridge Comments =====")
|
||||
|
||||
# Configure the server parameters
|
||||
server_parameters = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=["bridge_mcp_hydra.py"],
|
||||
)
|
||||
|
||||
# Connect to the bridge
|
||||
logger.info("Connecting to bridge...")
|
||||
async with stdio_client(server_parameters) as (read_stream, write_stream):
|
||||
# Create a session
|
||||
logger.info("Creating session...")
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
# Initialize the session
|
||||
logger.info("Initializing session...")
|
||||
init_result = await session.initialize()
|
||||
await session.initialize()
|
||||
|
||||
# First set the current instance
|
||||
logger.info("Setting current Ghidra instance...")
|
||||
await session.call_tool(
|
||||
"instances_use",
|
||||
arguments={"port": 8192}
|
||||
)
|
||||
|
||||
# Get a function to test with
|
||||
logger.info("Getting current address...")
|
||||
addr_result = await session.call_tool("get_current_address", arguments={"port": 8192})
|
||||
addr_result = await session.call_tool("ui_get_current_address")
|
||||
addr_data = json.loads(addr_result.content[0].text)
|
||||
|
||||
if not addr_data.get("success", False):
|
||||
@ -80,51 +91,46 @@ async def test_bridge_comments():
|
||||
address = addr_data.get("result", {}).get("address", "08000200")
|
||||
logger.info(f"Using address: {address}")
|
||||
|
||||
# Test normal comment
|
||||
logger.info("Testing set_comment with plate type...")
|
||||
logger.info("Testing comments_set with plate type...")
|
||||
comment_text = f"MCP PLATE COMMENT {int(time.time())}"
|
||||
result = await session.call_tool("set_comment",
|
||||
arguments={"port": 8192,
|
||||
"address": address,
|
||||
"comment": comment_text,
|
||||
"comment_type": "plate"})
|
||||
logger.info(f"set_comment result: {result}")
|
||||
result = await session.call_tool("comments_set",
|
||||
arguments={"address": address,
|
||||
"comment": comment_text,
|
||||
"comment_type": "plate"})
|
||||
logger.info(f"comments_set result: {result}")
|
||||
|
||||
# Test decompiler comment
|
||||
logger.info("Testing set_decompiler_comment...")
|
||||
logger.info("Testing functions_set_comment...")
|
||||
decompiler_comment = f"MCP DECOMPILER COMMENT {int(time.time())}"
|
||||
decompile_result = await session.call_tool("set_decompiler_comment",
|
||||
arguments={"port": 8192,
|
||||
"address": address,
|
||||
"comment": decompiler_comment})
|
||||
logger.info(f"set_decompiler_comment result: {decompile_result}")
|
||||
decompile_result = await session.call_tool("functions_set_comment",
|
||||
arguments={"address": address,
|
||||
"comment": decompiler_comment})
|
||||
logger.info(f"functions_set_comment result: {decompile_result}")
|
||||
|
||||
# Wait a bit and then clear comments
|
||||
await anyio.sleep(5)
|
||||
|
||||
# Clear the comments
|
||||
logger.info("Clearing comments...")
|
||||
await session.call_tool("set_comment",
|
||||
arguments={"port": 8192,
|
||||
"address": address,
|
||||
"comment": "",
|
||||
"comment_type": "plate"})
|
||||
await session.call_tool("comments_set",
|
||||
arguments={"address": address,
|
||||
"comment": "",
|
||||
"comment_type": "plate"})
|
||||
|
||||
await session.call_tool("set_decompiler_comment",
|
||||
arguments={"port": 8192,
|
||||
"address": address,
|
||||
"comment": ""})
|
||||
await session.call_tool("functions_set_comment",
|
||||
arguments={"address": address,
|
||||
"comment": ""})
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
"""
|
||||
Main entry point for comment tests.
|
||||
|
||||
Runs both HTTP API and MCP bridge tests sequentially.
|
||||
"""
|
||||
try:
|
||||
# First test HTTP API directly
|
||||
test_http_api_comments()
|
||||
|
||||
# Then test through MCP bridge
|
||||
anyio.run(test_bridge_comments)
|
||||
logger.info("All comment tests completed successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
logger.error(f"Error in comment tests: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the create_data function works properly.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import requests
|
||||
import time
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("create_data_test")
|
||||
|
||||
def wait_for_program_loaded():
|
||||
"""Wait for a Ghidra program to be loaded."""
|
||||
for _ in range(10): # Try for ~20 seconds
|
||||
try:
|
||||
response = requests.get("http://localhost:8192/program")
|
||||
if response.status_code == 200:
|
||||
data = json.loads(response.text)
|
||||
if data.get("success", False):
|
||||
logger.info("Program loaded: " + data["result"]["name"])
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking program status: {e}")
|
||||
|
||||
logger.info("Waiting for program to load...")
|
||||
time.sleep(2)
|
||||
|
||||
logger.error("Timed out waiting for program to load")
|
||||
return False
|
||||
|
||||
def test_create_data():
|
||||
"""Test creating data at different addresses with different types."""
|
||||
# First wait for a program to be loaded
|
||||
if not wait_for_program_loaded():
|
||||
logger.error("No program loaded, cannot test create_data")
|
||||
return False
|
||||
|
||||
# First get the memory map to find addresses where we can create data
|
||||
try:
|
||||
response = requests.get("http://localhost:8192/memory")
|
||||
memory_info = json.loads(response.text)
|
||||
|
||||
# Get valid addresses from an existing memory region
|
||||
memory_blocks = memory_info.get("result", [])
|
||||
|
||||
# Find a valid memory block
|
||||
valid_addresses = []
|
||||
for block in memory_blocks:
|
||||
if "start" in block and "name" in block:
|
||||
# Get starting address of a RAM block
|
||||
if "RAM" in block["name"].upper():
|
||||
# Use the first 10 bytes of this RAM block
|
||||
addr_base = int(block["start"], 16)
|
||||
for i in range(10):
|
||||
valid_addresses.append(f"{addr_base + i:08x}")
|
||||
break
|
||||
|
||||
# If no RAM blocks, try any memory block
|
||||
if not valid_addresses:
|
||||
for block in memory_blocks:
|
||||
if "start" in block:
|
||||
# Use the first 10 bytes of this block
|
||||
addr_base = int(block["start"], 16)
|
||||
for i in range(10):
|
||||
valid_addresses.append(f"{addr_base + i:08x}")
|
||||
break
|
||||
|
||||
# Fallback to known addresses if still nothing
|
||||
if not valid_addresses:
|
||||
valid_addresses = ["08000100", "08000104", "08000108", "0800010c",
|
||||
"08000110", "08000114", "08000118", "0800011c"]
|
||||
|
||||
logger.info(f"Will try using addresses: {valid_addresses[:3]}...")
|
||||
addresses = valid_addresses
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting memory map: {e}")
|
||||
# Fallback to some addresses that might be valid
|
||||
addresses = ["08000100", "08000104", "08000108", "0800010c",
|
||||
"08000110", "08000114", "08000118", "0800011c"]
|
||||
|
||||
# Try data types
|
||||
types_to_try = ["uint32_t", "int", "float", "byte", "char", "word", "dword", "string"]
|
||||
|
||||
success_count = 0
|
||||
|
||||
for i, data_type in enumerate(types_to_try):
|
||||
address = addresses[i % len(addresses)]
|
||||
logger.info(f"Testing data type: {data_type} at address {address}")
|
||||
|
||||
# First try direct HTTP API
|
||||
url = f"http://localhost:8192/data"
|
||||
payload = {
|
||||
"address": address,
|
||||
"type": data_type,
|
||||
"newName": f"TEST_{data_type.upper()}"
|
||||
}
|
||||
|
||||
# Add size for string types
|
||||
if data_type.lower() == "string":
|
||||
payload["size"] = 16
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload)
|
||||
logger.info(f"HTTP API - Status: {response.status_code}")
|
||||
logger.info(f"HTTP API - Response: {response.text}")
|
||||
if response.status_code == 200 and json.loads(response.text).get("success", False):
|
||||
success_count += 1
|
||||
logger.info(f"HTTP API - Success with data type {data_type}")
|
||||
else:
|
||||
logger.warning(f"HTTP API - Failed with data type {data_type}")
|
||||
except Exception as e:
|
||||
logger.error(f"HTTP API - Error: {e}")
|
||||
|
||||
# Short delay between tests
|
||||
time.sleep(0.5)
|
||||
|
||||
return success_count > 0
|
||||
|
||||
def main():
|
||||
try:
|
||||
result = test_create_data()
|
||||
if result:
|
||||
logger.info("Test successful!")
|
||||
else:
|
||||
logger.error("All test data types failed")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the delete_data functionality works properly.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import requests
|
||||
import time
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("delete_data_test")
|
||||
|
||||
def test_delete_data():
|
||||
"""Test deleting data."""
|
||||
# First create data at a specific address
|
||||
test_address = "08000100" # This should be a valid address in the memory map
|
||||
test_type = "byte"
|
||||
|
||||
# Step 1: Create some data
|
||||
logger.info(f"Creating test data at {test_address}")
|
||||
create_url = "http://localhost:8192/data"
|
||||
create_payload = {
|
||||
"address": test_address,
|
||||
"type": test_type,
|
||||
"newName": "TEST_DELETE_ME"
|
||||
}
|
||||
|
||||
try:
|
||||
create_response = requests.post(create_url, json=create_payload)
|
||||
logger.info(f"Create response: {create_response.status_code}")
|
||||
logger.info(f"Create response: {create_response.text}")
|
||||
|
||||
create_success = create_response.status_code == 200 and json.loads(create_response.text).get("success", False)
|
||||
|
||||
if not create_success:
|
||||
logger.warning("Failed to create test data, test may fail")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating test data: {e}")
|
||||
|
||||
# Short delay
|
||||
time.sleep(1)
|
||||
|
||||
# Step 2: Delete the data
|
||||
logger.info(f"Deleting data at {test_address}")
|
||||
delete_url = "http://localhost:8192/data/delete"
|
||||
delete_payload = {
|
||||
"address": test_address,
|
||||
"action": "delete"
|
||||
}
|
||||
|
||||
try:
|
||||
delete_response = requests.post(delete_url, json=delete_payload)
|
||||
logger.info(f"Delete response: {delete_response.status_code}")
|
||||
logger.info(f"Delete response: {delete_response.text}")
|
||||
|
||||
# Check if successful
|
||||
if delete_response.status_code == 200:
|
||||
response_data = json.loads(delete_response.text)
|
||||
if response_data.get("success", False):
|
||||
logger.info("Successfully deleted data!")
|
||||
return True
|
||||
|
||||
logger.warning("Failed to delete data")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting data: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
try:
|
||||
result = test_delete_data()
|
||||
if result:
|
||||
logger.info("Test successful!")
|
||||
else:
|
||||
logger.error("Test failed")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user