ghidra-cli/docs/research/ghidra-mcp-comparison.md

27 KiB

Ghidra MCP comparison for ghidra-cli

Status: reviewed and adopted into the PRD, 2026-07-28

Scope and source snapshots

This note compares the accepted ghidra-cli version 0.1 design with two primary-source repositories:

The comparison uses repository source, manifests, CI, licenses, changelogs, and the repositories' own issue reports. It does not treat README marketing claims as independently verified performance evidence.

Executive conclusion

Do not change the version 0.1 architecture or widen its command surface because of these MCP implementations. The accepted synchronous Rust harness, private file protocol, one Sample per command, immutable Analysis, strict JSON, and sandboxed short-lived Ghidra worker directly avoid the largest sources of complexity visible in both repositories.

The repositories nevertheless validate several useful mechanisms:

  • Relationship queries are the most valuable next capability for an agent. Prioritize strings, xrefs, and direct calls after version 0.1, followed by targeted disassembly and detailed single-Function inspection.
  • A large agent surface needs categories and capability discovery. Do not build that for four commands, but preserve command metadata so a future MCP adapter can expose a small, generated catalog without hand-written duplication.
  • Every process/protocol boundary needs independent size validation and parity tests. Add a small explicit ceiling for internal request.json, and test that Rust operation types, Java dispatch, and published schemas contain exactly the same operations.
  • Explicit target selection is essential. Keep requiring the Sample and exact Function Selector on every Query; never introduce a mutable “current program” fallback.
  • An eventual MCP integration should be a thin, separate adapter that invokes ghidr and forwards its schemas. It should not add an embedded HTTP server, persistent Ghidra session, or a second result contract to the core.

The principal rejection is breadth. One examined project exposes 272 catalog entries, including mutations, arbitrary scripts, project lifecycle, emulation, and debugging; its own users subsequently requested filtering because the surface was too large for agents (issue #267) and raised broader maintainability and provenance concerns (issue #307). The current ghidra-cli narrow vertical slice is a feature, not a gap.

LaurieWired/GhidraMCP

Architecture and state

The original project is a three-hop interactive topology:

MCP client -> Python FastMCP bridge -> HTTP -> plugin in a running Ghidra GUI

The Python process speaks MCP over stdio by default and can expose SSE; every tool then makes an HTTP request to a configured Ghidra URL (bridge initialization and transports, CLI transport selection). The Java extension embeds com.sun.net.httpserver.HttpServer in Ghidra and operates on the GUI's ProgramManager.getCurrentProgram() (server construction, current Program resolution). Installation therefore requires installing and enabling a GUI extension and opening Ghidra, in addition to Python and the MCP SDK (installation prerequisites and workflow).

This is session-oriented rather than content-oriented. The Sample is not an argument to a tool call; the implicit target is whichever Program is current in the GUI. Cursor-dependent tools expose the current address and Function, further coupling automation to interactive state (current-selection tool wrappers).

Tool surface and mutation

The snapshot has 27 Python @mcp.tool declarations mapped to 27 Java HTTP contexts. Read tools cover Functions, classes/namespaces, segments, imports, exports, data, strings, disassembly, and cross-references. Write tools rename Functions, data, and variables; change prototypes and local types; and add comments (Python tool declarations, HTTP context registration). Writes run Ghidra transactions on the Swing event thread, so this is not a read-only analysis layer (rename transaction).

Result contract and bounds

The outer MCP parameter schemas are inferred from Python type annotations, but the inner HTTP protocol is mostly newline-delimited text. safe_get splits text into lines, while failures are returned as strings in the same value channel rather than typed MCP errors (GET behavior, POST behavior). The Java server always responds with HTTP 200 and text/plain, including application errors (response writer).

Many lists have offset and limit, normally defaulting to 0 and 100, but there is no total, returned, or has_more metadata. Some duplicate tools are unpaginated, such as list_functions (list wrapper). Pagination accepts negative and arbitrarily large values without a public validation contract (pagination implementation). There is no whole-response byte bound or spill mechanism.

Function selection is ambiguous by construction: name selection returns the first exact basename encountered, and address selection accepts an interior address by resolving the containing Function (name selection, interior-address fallback). These are precisely the ambiguities the accepted ghidra-cli selector contract should continue rejecting.

The timeout layers are inconsistent. The Python bridge times every HTTP call out after five seconds, while Java decompilation can run for 30 or 60 seconds (bridge timeouts, decompiler timeout). An MCP caller can therefore receive a timeout while Ghidra continues working. This validates ghidra-cli's single derived child watchdog and process-group termination design.

Security, tests, and dependencies

new InetSocketAddress(port) binds the embedded server without an explicit loopback address, and the request path shown has neither authentication nor a body-size cap (server bind, unbounded form read). The Ghidra process and plugin are unsandboxed and inherit the GUI user's filesystem and network access.

Runtime dependencies are Python 3.10+, mcp, and requests; the Java build uses Maven plus manually copied Ghidra JARs (Python requirements, system-scoped Ghidra JARs). The only committed Java test is the Maven-template assertTrue(true) smoke test, so no endpoint, schema, timeout, security, or real-Ghidra regression is covered (test source).

bethington/ghidra-mcp

Architecture and state

This repository identifies itself as a substantial derivative of the Laurie project, not an independent implementation (NOTICE). It retains the bridge topology but expands it:

MCP client -> Python MCP bridge -> UDS or HTTP -> Ghidra GUI plugin
                                             -> headless Java server
                                             -> optional debugger service

The bridge supports stdio, Streamable HTTP, and deprecated SSE (bridge CLI), while repository documentation describes separate GUI and headless Java servers and a Python protocol-conversion process (architecture).

The bridge discovers server instances, remembers a connected project and transport, dynamically registers tools, and attempts reconnection after Ghidra restarts (shared mutable state, reconnection). The cost is visible in the project's own issue history. Concurrent sub-agent calls caused intermittent bridge shutdown and forced a serialization lock (issue #91, current lock). On macOS, a GUI/agent environment mismatch in $TMPDIR prevented Unix-socket discovery until the implementation learned to scan multiple candidate locations (issue #170, current discovery rationale).

These are real solutions for a persistent multi-instance service, but they are states ghidra-cli intentionally does not have.

Tool surface, discovery, and mutation

The committed endpoint catalog declares 272 tools across 15 categories (catalog). The surface includes read-only listing, Functions, cross-references, call graphs, strings, data flow, and memory inspection, but also symbol/type/comment mutation, project deletion and version control, arbitrary Java script execution, P-code emulation, and live debugging (feature inventory, API categories).

Its most reusable mechanism is annotation-driven registration. Java service methods carry tool/parameter annotations; one scanner creates deterministic HTTP endpoint definitions and a machine-readable input schema (scanner contract and sorting, descriptor construction). The Python bridge fetches that schema and generates callable MCP signatures at runtime (dynamic handler construction, schema fetch).

The current bridge can load categories lazily and exposes search_tools, list_tool_groups, and load/unload/check helpers (agent discovery workflow). This directly addresses tool overload, but it also introduces another mutable session dimension and depends on clients honoring tools/list_changed.

Result contract and bounds

The Java response type is an improvement over the original but remains a union of structured success, simple { "error": "message" }, and raw text passthrough (sealed Response). Consequently, some endpoints return JSON while many paginated endpoints still return newline-delimited text, and the dynamically generated MCP handlers all declare str results (text pagination, generated return annotation). The live tool schema describes inputs, not stable versioned output schemas or typed error codes (schema translation).

Request hardening is much stronger: version 6.0 applies a 64 MiB body ceiling and bounds actual reads rather than trusting Content-Length (request ceiling, HTTP enforcement). Response bounding remains endpoint-specific rather than a universal contract. For example, the whole-program call graph accepts limit=0 as unlimited and can emit text, DOT, Mermaid, adjacency, or address-edge JSON (call-graph parameters and implementation). ghidra-cli's one schema, disclosed collection bounds, and universal serialized byte cap are materially stronger for an automation agent.

The bridge coordinates endpoint-specific HTTP timeouts and retries GET calls; write calls deliberately avoid blind retry because completion is ambiguous after a connection loss (timeout catalog, retry policies). Notably, the bridge's decompile_function timeout is 45 seconds while the Java endpoint defaults to 60 seconds (bridge value, Java value). This is further evidence that one owning harness should derive and enforce the complete child lifetime.

The project learned an important target-selection lesson. Calls default to a mutable active Program; strict explicit Program selectors exist only behind an environment variable because compatibility retained the fallback (strict-mode rationale, bridge enforcement). ghidra-cli should keep Sample selection mandatory from its first release and never acquire this compatibility burden.

Security, tests, and dependencies

Current security controls include loopback defaults, refusal of a non-loopback bind without bearer authentication, anti-CSRF/DNS-rebinding checks, script execution disabled by default, filesystem-root containment, request-body caps, and generic uncaught-error responses (security configuration, safe handler). The change history states that these were hardening changes after earlier unauthenticated, ungated, or insufficiently bounded behavior (v6.0.0 security history).

These controls are valuable for a network service, but they do not isolate Ghidra from the user's account. ghidra-cli should retain the stronger design of no listener plus a capability-limited worker sandbox. If it ever adds an MCP adapter, stdio should remain the default and network transport should be a separately threat-modeled feature.

The shipped bridge requires Python 3.10+ and the mcp package; tests and unshipped subsystems add many optional dependencies (package manifest, dependency groups). The repository has a materially stronger regression practice than the original: Java offline tests and coverage, a Python version matrix, Windows-specific tests, performance regressions, and Pester setup tests are gating; formatting and lint jobs are currently advisory (CI gates, quality jobs). Its annotation/schema/catalog parity approach is worth borrowing; its broad multi-subsystem matrix is not needed for version 0.1.

Recommendations for ghidra-cli

Adopt now

  1. Keep one source of truth for operations. Define every internal operation once with its request type, response type, public command kind, and schema name. Generate or mechanically verify the Rust schema set and Java dispatch set against it. The bethington annotation scanner demonstrates the value of preventing hand-maintained endpoint/schema drift, while ghidra-cli can do this without runtime reflection or dynamic registration.

  2. Cap both sides of the private protocol. The accepted 256 MiB response ceiling is good. Add a much smaller fixed serialized request.json ceiling, suggested at 1 MiB, enforced by Rust before launch and Java before parse. Requests contain metadata and tool-owned paths, not Sample bytes, so a larger allowance has no known use. This is a protocol-hardening detail, not a public CLI flag.

  3. Add protocol/catalog parity regressions. A fixture should fail if a Rust operation lacks Java handling, if Java accepts an unpublished operation, or if a public schema/golden is missing. Keep the already accepted real-Ghidra ELF and PE integration tests and strict lint/format gates.

  4. Retain mandatory targets and stable identifiers. Every Query keeps an explicit Sample; Function operations keep exact entry Address or exact name selection with ambiguity errors. Do not add GUI cursor, active Program, or implicit containing-Function behavior.

  5. Preserve extension metadata without exposing it yet. Command/domain definitions may carry a stable category and one-sentence agent description. This costs little and lets a later MCP adapter generate a discoverable surface, but version 0.1 needs no search-tools, runtime group loading, or config file.

Defer in priority order

  1. strings Query. Return a bounded, stably ordered list with exact Address, byte length, decoded representation with explicit encoding, and referring Addresses or a separate bounded reference link. Avoid case-folded server-side substring filters initially; agents can use jq, and raw Sample-derived strings may be non-UTF-8 or extremely large.

  2. xrefs Query. Model each Reference structurally: source Address, destination Address or external target, Ghidra reference type, operand index, and containing source Function entry when present. Direction should be an explicit enum. Never emit prose such as From ... in ... [READ].

  3. Direct calls Query. Start with flat Call Edges containing caller entry, callsite, and resolved or unresolved callee. Support pagination and stable ordering. Do not begin with recursive whole-program graphs, DOT/Mermaid renderers, graph algorithms, or limit=0 unlimited output; agents can build graphs from bounded edge pages.

  4. Targeted disassemble and function detail Queries. These are more useful than widening every functions item. A detailed Function result can later carry signature, parameters, locals, body ranges, thunk resolution, and alternate symbols without making discovery pages expensive.

  5. Batch Query execution. Multiple decompilations in one Ghidra process may eventually reduce startup cost, but only add it after measurements. It needs explicit per-item success/error objects, deterministic order, aggregate byte bounds, cancellation semantics, and no hidden parallelism.

  6. Thin MCP adapter. After schemas and CLI behavior stabilize, a separate package can expose one MCP tool per stable ghidr Query, invoke the CLI over stdio/process execution, and forward its JSON unchanged. If the surface later grows, generated categories and a capability-search tool become appropriate. The adapter should not call Ghidra directly or own Analysis Store policy.

  7. Interactive/mutating analysis. Renames, types, comments, scripts, debugging, and shared-project workflows solve a different problem. If ever pursued, put them behind a separate explicit write-capability model and a separate domain contract; do not weaken immutable Analysis semantics.

Explicitly reject for version 0.1

  • Embedded HTTP, TCP, UDS, SSE, Streamable HTTP, discovery scans, reconnect logic, persistent daemons, and background JVMs.
  • Mutable current-Program/current-Address/current-Function session state.
  • Any automatic first match for duplicate Function names or interior addresses.
  • Raw text success/error unions, HTTP-status tunneling, untyped addresses, and prose records that agents must parse.
  • Hidden retry escalation. A caller may explicitly retry a failed read-only Query; ghidr must not multiply time budgets behind the contract.
  • Arbitrary Java/Python scripts, mutation, project management, debugger control, emulation, binary export, and Ghidra Server administration.
  • Whole-program graph renderers and custom search/query languages. Emit bounded facts; let jq and downstream tools transform them.
  • Runtime tool-group loading for the initial four-command surface.
  • A database or global mutable index merely to support discovery or MCP state.

Licensing and provenance

Both repositories use Apache-2.0 (LaurieWired license, bethington package license). The bethington repository explicitly retains attribution to LaurieWired for portions that remain (NOTICE).

Concepts, public Ghidra API usage patterns, and independently designed schemas may be studied without making ghidra-cli a derivative. If any source or close adaptation is copied, Apache-2.0 requires preserving applicable copyright, license, change, and NOTICE obligations. The cleanest course is to keep the implementation independent, cite these projects as prior art in research and design notes, and record provenance for any deliberately reused code before it enters a commit.

The open maintainability issue also alleges that some contributor changes were recommitted in ways that obscured authorship. This report does not adjudicate that allegation, but it reinforces the accepted ghidra-cli rule: AI assistance does not replace review, attribution, or a traceable change history (issue #307).

Decision impact

No accepted version 0.1 product decision should be removed. The research adds three concrete implementation-time checks and a clearer post-0.1 order:

  • internal request-size ceiling;
  • operation/Java/schema parity test;
  • relationship-query order: strings, xrefs, direct calls, then targeted disassembly and Function detail.

Everything else belongs either in a future thin MCP adapter or in a separately designed interactive/mutating product.

The PRD now records the request ceiling, operation/dispatch/schema parity, no-default-egress regression coverage, ordered relationship Queries, bounded batch prerequisites, and thin-adapter MCP boundary. These adoptions do not add a public version 0.1 command or widen the Java adapter's authority.