Blog
ai-agentmcpreverse-engineeringghidraclaude-codeagent-tools

Inside REA — an MCP Server That Lets AI Agents Reverse Engineer Software

A source-level analysis of morluto/rea (Reverse Engineer Anything), which passed 240k GitHub stars in six months: layered architecture, the Evidence and Unknown ledgers, Hopper/Ghidra/IDA provider bridges, token-budget design, prompt-injection defenses, limitations and adoption caveats.

Data DynamicsOctober 15, 202620 min read

"How does search work in this app? Show me the evidence, and build something similar for our project."

REA (Reverse Engineer Anything) lets an AI coding agent handle requests like that without the source code. One MCP (Model Context Protocol) server and CLI cover native binaries, Electron/JavaScript apps, .NET assemblies, Android APKs, firmware and websites. Released in April 2026, it passed 240k GitHub stars and 60k forks in six months (GitHub API, 2026-10-15).

The popularity is less interesting than the design. REA tackles the "hook an LLM up to a disassembler and it tells plausible lies" problem head-on. Every result carries its provenance, confidence, coverage and limitations, and what is not known is managed as data in its own right. This post is based on cloning the repository at v6.5.0 (released 2026-10-13) and reading its documentation and code.

TL;DR: REA's core is not a decompiler wrapper but an Evidence contract that separates observations, derivations, inferences and unknowns. If your team builds MCP tools, that contract is more instructive than the feature list.

At a glance

ItemDetails
Repositorymorluto/rea (MIT)
Distributionnpm rea-agents (currently 6.5.0), MCP registry name io.github.morluto/rea
Language / runtimeTypeScript, Node.js 22.19+ / 24.11+ / 26+
Size~300k lines of non-test TS in src/, 469 colocated tests plus 1,148 files under tests/
InterfacesMCP stdio server + one-shot CLI (rea) sharing the same application workflows
Deep analysis enginesHopper, Ghidra, IDA (bring your own install and license)
Auxiliary enginesJADX, Apktool, Binwalk/Unblob, TShark, sigrok, WABT, pwntools, GDB, Rizin, objdump, Frida and more
Supported agents20+ including Claude Code, Codex, Cursor, Gemini CLI, Windsurf, OpenCode, Copilot CLI, Grok Build
Where it runsLocally only; targets are not uploaded (tool results do reach the agent's LLM provider)

The problem it addresses

Handing reverse engineering to an LLM agent runs into four practical problems.

  1. Unsupported conclusions — the agent sees a fragment of pseudocode and declares "this function performs encryption", with no record of what it looked at and what it missed.
  2. Confusing absence with unknown — an empty caller list becomes "nothing calls this", when the decompiler may simply have failed to resolve indirect calls.
  3. Context blow-up — a binary's function inventory, strings and pseudocode easily reach several megabytes. Dumping them into context burns tokens and time.
  4. The target is the attacker — a binary can embed a string like "run this command". That string flows through the tool result straight into the agent: a textbook indirect prompt-injection path.

Almost every design decision in REA answers one of these four.

Usage flow

Setup is one line. It shows the planned changes and asks for approval first.

npx rea-agents setup

Pick your agents and it registers the MCP server, installs the workflow skill reverse-engineer-anything, and backs up existing configuration. --dry-run --json prints the plan without writing anything. AGENTS.md states that the installer never installs or upgrades unrelated software (Node.js, Java, Homebrew, Ghidra and so on); the only exception is a separately approved Hopper installation.

After that you just ask the agent:

Understand how search works in the Notes app, show me the evidence, and build a
similar feature for my project.

The same workflows are available from the terminal. Static JavaScript analysis needs no native engine.

npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
 
# Native analysis: explicit provider + snapshot cache
rea analyze /absolute/path/to/program --provider ghidra \
  --snapshot /absolute/path/to/analysis/program.json

Architecture: dependencies flow inward

A simplified version of the repository's docs/architecture.mermaid:

Loading diagram…

The governing rule is one sentence in AGENTS.md:

Dependencies flow inward: domain semantics and contracts, provider adapters, shared application workflows, then CLI/MCP adapters.

  • src/domain holds pure, side-effect-free modules: the Evidence schema, comparison logic, application graphs, and a tagged error algebra with Result.
  • src/contracts holds the Zod schemas the caller (the agent) sees. Tool inputs, outputs and effect declarations live here and are converted to MCP JSON Schema.
  • src/application holds workflows shared by the CLI and MCP. Neither wraps the other; both are thin adapters over the same workflows, which structurally reduces "works in the CLI but not over MCP" drift.
  • Provider adapters contain engine protocols and parsing. Hopper's socket protocol and Ghidra's HeadlessScript output format never leak into the application layer.
  • src/process owns lifetime, deadlines and cleanup for every external process (more below).

Design 1 — Evidence: every result is a record with provenance

What an REA analysis tool returns is not "a result" but an Evidence record. The schema in src/domain/evidence.ts makes the intent clear (excerpt):

const evidenceBaseSchema = z
  .object({
    evidence_id: prefixedDigestSchema("ev"),
    subject: subjectSchema.nullable(),      // name, sha256, path, format, architecture
    provider: providerSchema,               // id, name, version
    analysis_profile: analysisProfileSchema.nullable(),
    predicate_type: z.string().min(1),
    operation: z.string().min(1),
    parameters: jsonObjectSchema,
    raw_result: jsonValueSchema.nullable(), // engine's original representation
    normalized_result: jsonValueSchema,     // provider-neutral result
    confidence: z.enum(["observed", "derived", "inferred"]),
    authority: evidenceAuthoritySchema,
    environment: executionEnvironmentSchema.nullable(),
    limitations: z.array(z.string()),
    relationship_coverage: relationshipCoverageSchema.nullable(),
    locations: z.array(evidenceLocationSchema),
    evidence_links: z.array(prefixedDigestSchema("ev")),
  })
  .strict();

The fields worth noting:

  • confidence: observed / derived / inferred — what the engine saw, what was mechanically computed from that, and what was inferred. The skill and MCP prompts repeatedly ask the agent to keep this distinction in its answers.
  • authority — one of shipped-artifact, controlled-replay, historical-reference, external-service, analyst-inference. "Seen in the shipped binary" and "an old open-source version did this" carry different weight, and the type system says so.
  • evidence_id — a digest over the canonical JSON of the record. Imports recompute it and reject records whose content does not match. Same question, same bytes, same settings yield the same ID, so it doubles as a cache key.
  • locations — a discriminated union of address, address range, file offset and in-artifact path, so every conclusion can be traced back to an exact spot in the original.
  • evidence_links — connects derived results to their sources. Summary views point to the full analysis via parent_evidence_id.

Evidence is retained in the session bundle and can be exported, imported and compared. For instance, two analyze_function Evidence records can be passed directly to compare_functions to diff a function across versions.

Comparison is reluctant to say "equal"

A function dossier declares, per facet (pseudocode, assembly, callers, callees, references, cfg …), whether it was observed:

"facet_availability": {
  "pseudocode": { "available": true },
  "callees":    { "available": true },
  "references": { "available": false, "reason": "provider does not classify reference kinds" }
}

If either side did not observe a facet, compare_functions leaves that dimension unknown with null counts. An empty array that was observed is treated differently from one that was never observed. In the docs' words: "Missing coverage is unknown, not absence."

Artifact comparison works the same way. unchanged requires matching scopes, complete graph members and complete traversal coverage, and the contract explicitly notes that static equality does not establish unchanged runtime behavior.

Design 2 — The Unknown ledger: not knowing as first-class data

REA tracks unresolved questions as residual unknown records with unk_-prefixed IDs (src/domain/residualUnknown.ts). The ID hashes the whitespace-normalized question, its domain and a scope digest as canonical JSON, so registering the same question twice converges on one record.

Each unknown carries the confidence required to resolve it (required_confidence) and probes (which operation to run, and why). Agents manage the ledger with record_unknown, update_unknown and list_unknowns; the MCP prompt audit_residual_unknowns walks through the remaining unknowns and which ones Evidence can legitimately resolve.

Call-graph tracing follows the same philosophy. trace_call_path records blocked expansions in residual_unknowns and sets truncated: true. If the goal is not reached but an expansion is unresolved, goal_status is unknown, not not_reached. It distinguishes "looked and it isn't there" from "couldn't finish looking".

The ledger pays off when reverse engineering leads to reconstruction. The verify_reconstruction prompt and the reconstruction obligation ledger check each item of a reimplementation spec against retained comparison Evidence. The DX-Ball showcase reports a sound-pan calculation reconstructed in C that passes 3,205 original-x86 cases and reproduces all 63 compiled function bytes.

Design 3 — Tokens are the first-order cost

The performance section of AGENTS.md fixes the optimization order:

  1. Agent tokens and context — the smallest sufficient answer with necessary provenance, coverage and limitations, counted over the whole task including schemas, extra calls and retries.
  2. Analysis time — reuse validated results; cache keys must cover every result-affecting input.
  3. CPU and memory.

It also warns that "fewer bytes alone do not establish fewer tokens" and requires measuring real tasks. The mechanisms:

  • Response budget — results, including text, structured content and the JSON-RPC envelope, fit within the pinned stdio client's 10 MiB receive buffer. If not, REA returns resource_constraint before building a document-sized string, keeps the complete Evidence in the session and reports its reference. Clients with a larger buffer can match it with REA_MCP_MAX_RESPONSE_BYTES.
  • detail: "summary" — large analyses (JavaScript apps, binary layout, artifacts, WASM …) record the complete Evidence in the session and return only summary Evidence.
  • inspect_analysis_view — re-reads retained Evidence as a summary, a single section/module, or stable pages without rerunning analysis or starting a provider.
  • Continuations — for oversized pseudocode, the error details carry a ready-to-call inspect_analysis_view request (name and arguments). The agent doesn't have to work out how to ask for the next page.
  • Snapshot cache — results are reused only when target bytes, operation, parameters, provider and settings match exactly. Dispatching a metadata write invalidates the cache even if the reply fails, because the engine may already have applied the write.

Design 4 — Provider bridges

REA does not implement a decompiler. It launches existing engines as owned processes and bridges to them.

Hopper: a Python adapter on the engine's own thread

bridge/hopper_bridge.py (~51 KB) runs inside Hopper via its --python launcher option. A bootstrap injects the REA_SOCKET path and a random REA_TOKEN; the bridge opens a permission-restricted (0600), single-client Unix socket and checks the token with hmac.compare_digest. The module docstring carries a warning:

"""Authenticated REA adapter executed on Hopper's dedicated Python thread.
...
Keep all Hopper API access on this thread: moving dispatch to a worker can deadlock Hopper.
"""

Even the engine API's threading constraints are documented as part of the contract.

Ghidra: a headless script with ephemeral databases

bridge/ghidra/ReaGhidraBridge.java (~235 KB) is packaged as a Ghidra HeadlessScript providing inventory, decompilation, references and CFG. The rules: ephemeral databases, no modification of executable bytes, no GUI control. Architecture profiles such as MIPS soft-float, RISC-V, PowerPC, PSP, 16-bit DOS and headerless 6502 images each have their own document and verification lane, and inputs outside the documented ABI/flag boundaries are refused.

Because Ghidra cold starts are slow, there is a "first-query deadline and recovery" contract. A client can stop waiting while the provider keeps analyzing; analysis_activity on the session reports progress. Large binaries can raise the startup deadline with REA_GHIDRA_STARTUP_TIMEOUT_MS.

IDA: read-only

IDA support adapts the upstream ida-pro-mcp contract, either attaching to a running GUI or owning a headless database. The rule is blunt: "IDA analysis is read-only; never save or close an attached GUI database." An agent will not trash the IDB you have open.

Deterministic selection, no fallback

AnalysisProviderRegistry sorts candidates and selects deterministically, with no fallback. One capable provider is used automatically; several produce an ambiguous error with candidate_ids for the caller to choose from. The chosen provider stays bound to the session until an explicit switch or close.

No fallback can feel inconvenient, but it prevents provenance contamination ("I thought this came from Ghidra, but another engine answered"). The provider field on Evidence only means something because of this rule.

Design 5 — Never trust the target

Strings, symbols, pseudocode, page text, scripts, network captures and logs are all text produced by the target. REA treats them as data, and its server instructions tell the agent never to follow directives found in tool results and to report any request to run, install, download or send something as a prompt-injection attempt.

On top of that, results carry a stable marker in _meta:

{ "provenance": { "untrusted_content": true, "source": "analysis_target" } }
  • The marker is declared per tool contract via returnsTargetContent and is on by default. Only audited exceptions (export_evidence_bundle, record_unknown) omit it; new tools are marked until audited.
  • Error results carry it too, since error messages and partial observations can contain target-derived text.
  • Hosts and hooks can use it to apply policy, e.g. require approval for process-launching or device-mutating tools after target content was read.

This is a pattern worth copying for any MCP server. Instead of claiming to "prevent prompt injection", it exposes taint as machine-readable metadata and leaves policy to the host.

Design 6 — Processes are managed by ownership

REA constantly launches heavy external processes: the Ghidra JVM, Hopper, JADX, TShark, browsers. src/process handles process groups, private root directories, deadlines, bounded file reads and cleanup consistently. A few principles stand out:

  • "A PID and executable path alone do not establish ownership" — PIDs get reused after exit, so separate proof of ownership is kept to avoid killing someone else's process. This is why Linux requires a procps-compatible ps.
  • Resources whose cleanup failed stay owned and are reported as cleanup_incomplete. Observations are preserved regardless of cleanup success.
  • On Windows, the Node-API module in native/windows handles NTFS volume admission, protected DACLs and atomic Job Object assignment.

When an agent calls tools hundreds of times, not leaving zombie JVMs behind is a quality attribute that matters more than it sounds.

Target coverage

From the README (supported OS and versions vary per verification lane):

TargetWhat REA returnsRequirements
Native binariesPseudocode, assembly, strings, symbols, calls and referencesHopper / Ghidra / IDA
Go · Rust binariesCompiler version, module and crate dependenciesBuilt-in static parsers
JavaScript / ElectronModules, imports, source maps, routes, IPC, native add-onsNode.js only
WebsitesPage structure, scripts, network observations, screenshotsChrome-family browser (CDP)
Saved network capturesRequests, responses, payloads, source locationsHAR, Fiddler SAZ, mitmproxy
.NET assembliesMetadata, CIL, native dependencies, build comparisonsNone (static)
Android APKs and devicesManifest, classes, decompiled methods, logs and dumpsJADX + JDK, adb
Flutter · HarmonyOSDart snapshot hash, engine build, package structureNone (static)
FirmwareRegions, extraction results, native-analysis handoffsBinwalk / Unblob (Linux)
WASM · EVM bytecodeSections, import/export WAT, dispatch selectorsWABT 1.0.42, Linux x64
SQLite · packets · logic signalsTables and rows, protocol fields, decoder annotationsNode 24.15+, TShark, sigrok
Crash dumps · process behaviorThread registers, terminal output, filesystem observationspwntools, PTY

The MCP surface: tools, prompts and a skill

The server code contains 127 registerTool call sites (some registrations are conditional, so the exposed set depends on session and providers). For a running server, binary_session is the authoritative catalog; from source, npm run docs:generate produces product-catalog.json.

The tool design guide (docs/tool-design.md) starts by choosing a tool's shape:

ShapeUseResult
inspectFacts about one targetRelevant fields, locations, facet availability
search / listCandidate discoveryStable ordering, pagination when needed
traceRelationships across evidenceTyped edges, unresolved paths
compareExplicitly paired artifactsIdentity, comparable coverage, deltas
workflowRecurring composite outcomeInline answer, contributing Evidence, partial facets
observe / captureRuntime behaviorAuthority, launch/attach effects, cleanup status

Other principles worth noting: prefer primitives and compose workflows only once repeated use proves the need; "an assumed agent budget is insufficient justification for a limit"; custom orchestration languages or replay engines only for an observed requirement.

Six provider-neutral workflows are exposed through the MCP prompts capability:

PromptPurpose
investigate_featureTrace a feature from discovery into function Evidence
compare_application_versionsCompare two shipped artifacts
verify_reconstructionCheck a reconstruction spec against comparison Evidence
trace_crashCorrelate a crash with static paths and process captures
audit_residual_unknownsAudit remaining unknowns and resolution evidence
prepare_bounded_process_captureDesign a bounded process experiment before running it

Prompts never call tools themselves; they only offer starting points. Prompt arguments are rendered as untrusted selection data, not instructions.

The reverse-engineer-anything skill opens with when not to use REA: if a complete source repository is available, use normal repository tools and skip REA diagnostics. When tools are missing it separates registration problems, restart problems and missing engines, each with its own remedy. The skill spends more words on when not to use tools and how to branch on failures than on tool usage itself, which is refreshingly practical.

Engineering culture: "if we didn't verify it, we say so"

The attitude of the docs and release notes is as striking as the code. Nearly every v6.5.0 changelog entry reads like this:

Recorded real-engine coverage uses Linux and Wireshark 4.6.6; native Windows/macOS and Bun behavior remain unverified.

Recorded real coverage is the Linux local Node marker workflow; macOS, Windows and remote devices/servers remain unverified. Instrumentation executes foreign code and is destructive, not read-only.

Features ship together with which OS and engine versions were actually verified and what was not. docs/testing.md defines real-toolchain verification lanes (Ghidra DOS, NativeAOT, IDA MCP, Android, packet captures …) and states that simulated providers do not establish real-provider behavior. The preferred test order is public CLI/MCP workflows, then production-boundary integration, then producer goldens, then module tests.

There is also Stryker mutation testing, jscpd duplication checks, knip for unused code, Turborepo cached builds and release-please with Conventional Commits. AI-assisted PRs are welcome, but a human must own the final diff and declare it ready, and AGENTS.md tells agents never to attest to human review on someone's behalf.

Showcases

The README highlights three case studies:

  • DX-Ball — follow a sound call into its position-to-pan helper, inspect the instructions and turn incomplete pseudocode into C. Passes 3,205 original-x86 cases and reproduces all 63 compiled function bytes.
  • Notion — find the renderer's clipboard API, follow it through preload and IPC into the main process, and inspect the rich clipboard format (Electron analysis).
  • TH04 — recover fixed and aimed bullet-ring angle calculations from a PC-98 game's 16-bit instructions and compare the reconstructed C++ with historical compiler output.

The common loop: pick one feature, follow the evidence down, reimplement, and check against the original. That is exactly the workflow REA is built for.

Limitations and adoption caveats

1. Legal boundaries are your responsibility. DISCLAIMER.md says the project does not support circumventing DRM, license checks or anti-cheat, building game cheats, bypassing authentication or bot protection, or copying proprietary code and assets. Reverse engineering may be restricted by jurisdiction and by the target's license. Settle purpose and authorization (interoperability, security research, software you own or are authorized to examine) first.

2. Commercial engine licenses are separate. REA grants no license for IDA Pro/Hex-Rays, Hopper or JEB, and trial/demo automation and output-sharing limits follow vendor terms. Ghidra is the realistic free starting point.

3. "Local analysis" is not "nothing leaves the machine". Analysis runs locally, but tool results (pseudocode, strings …) reach your LLM provider through the agent. For confidential binaries, review this against air-gapped LLM options and internal policy.

4. Runtime tools are not read-only. Frida instrumentation, process capture, browser scenarios and adb device operations execute target code or change state. Each tool declares its effects, but approval policy has to be enforced by the host.

5. It moves very fast. Minor releases within 6.x have BREAKING CHANGES sections, and 6.1→6.2 has its own migration guide. If you build a custom client, pin a version (rea-agents@6.5.0) and upgrade deliberately.

6. Verification varies by platform. Many features only have recorded verification on Linux x64. Windows Ghidra is P0 (experimental) without mutation authority. Check the changelog and testing docs for your exact combination.

7. Check the official source. The README warns that a similar repository name or copied README does not establish an official download. Lookalike malicious packages are common for popular projects; use only morluto/rea, rea.tools and npm rea-agents.

What we take away

For a data platform team there are more direct uses than you might expect: confirming the behavior of a vendor JDBC/ODBC driver without source, working out the I/O formats of a legacy batch binary, mapping the IPC of an internal Electron-based ops tool, or diffing binaries before and after an upgrade. These are interoperability and incident-analysis uses (after checking licenses and authorization, of course).

The bigger takeaway, though, is MCP tool design. These lessons apply directly to in-house agent tools for log analysis, query diagnostics or cluster health checks:

  1. Wrap results in Evidence — returning provenance (provider and version), confidence (observed/derived/inferred), locations and limitations turns hallucinations into verifiable claims.
  2. Type the difference between unknown and absent — put "empty because none" versus "empty because not looked at" into the contract. A single coverage: complete | unknown field prevents a lot of wrong conclusions.
  3. Compute big, return small — retain full results in the session, return summaries and page references, and hand back a ready-to-call continuation.
  4. Expose taint as metadata — mark tools that return external data with something like untrusted_content by default, and let hosts and hooks decide approval policy.
  5. Record verification scope honestly — "verified on X, the rest unverified" builds more trust than "supported".

Wrapping up

REA turns the simple idea of "agent plus decompiler" into a contract-centric system that preserves provenance and uncertainty end to end. Most of its ~300k lines go not into features but into the "boring" parts: boundaries, ownership, coverage and cleanup. That seems to be precisely why it holds up in real use.

If you are building tools that let AI agents "analyze" anything, read three files from REA: docs/tool-design.md, docs/mcp-contracts.md and src/domain/evidence.ts.

References