Skip to content

Architecture

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 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.

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.

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.

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.

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.

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.