REAgraph now connects to REA at runtime (MCP client, no vendored REA code):
- rea_client.py ReaClient implements bridge.ReaSource; spawns/connects to REA
via $REAGRAPH_REA_CMD (default: npx -y rea-agents@latest), lists
tools, calls analyses, yields fragments. mcp lazy-imported;
connect-vs-logic split so it tests offline; fail-open per tool.
- rea_adapter.py REA JavaScript Application Graph -> node-link fragments, grounded
against morluto/rea's real JAG schema (node_id/observations/edges/
relation/epistemic 'state'); fail-open to an evidence node on any
unknown tool or garbage shape; never raises.
- cli: 'reagraph pull <target>' connects to REA and streams findings into the
graph via bridge (crash-safe). Full CLI: add/diff/stats/summary/coverage/
query/pull/serve.
- examples/fake_rea_mcp_server.py + rea_live_demo.sh exercise the whole loop
offline (no REA/node); degrades to 'reagraph add' when mcp is absent.
- docs/REA_INTEGRATION.md; DESIGN note on optional future acceleration.
42 tests pass. Independent project; not affiliated with REA.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
9.1 KiB
REA integration
This document explains how REAgraph connects to REA. The short version: REAgraph does not contain REA. It talks to a running REA over the Model Context Protocol (MCP), reads the findings REA produces, and folds them into a persistent graph on disk. REA stays REA; REAgraph stays the thin memory and structure layer on top.
The wiring described here is in the tree and tested: reagraph.rea_client (the
MCP client), reagraph.rea_adapter (the result adapter, grounded against REA's
JavaScript Application Graph schema), reagraph.bridge (incremental merge), and
the reagraph pull <target> command. The mcp package is lazy-imported, so the
rest of REAgraph works without it, and examples/fake_rea_mcp_server.py lets you
exercise the whole loop offline (see examples/rea_live_demo.sh).
The integration model: client, not vendor
REAgraph does not vendor REA's source. REA is an MCP server; REAgraph is an MCP client. At analysis time REAgraph spawns a REA process on demand, speaks MCP to it, calls REA's analysis tools, and collects the results. The default spawn command is:
npx -y rea-agents@latest
That default is overridable. Set the REAGRAPH_REA_CMD environment variable to
point at any command that starts a REA MCP server (a pinned version, a local
checkout, a wrapper script, a container entrypoint). REAgraph runs whatever that
variable names and talks MCP to it; it does not care how REA was installed.
Why integrate at the protocol boundary instead of copying REA's code:
- Licensing. REA is MIT, graphify is Apache-2.0, and REAgraph is MIT. Keeping the three as separate projects that talk over formats and protocols keeps each license clean and avoids mixing code under different terms into one tree.
- Maintenance. Consuming REA's output rather than forking it means REAgraph rides REA's upstream. Improvements to REA's analysis arrive for free; REAgraph only maintains the thin adapter and client in between. The same logic applies on the query side, where REAgraph delegates to graphify rather than embedding it.
If you want REA's source checked out locally for development (to read it, pin it, or patch it), the clean option is a git submodule. A submodule keeps REA's history and license separate and under REA's own path, so nothing about REAgraph's licensing or provenance changes. It is a convenience for developers, not a dependency: REAgraph still talks to REA over MCP at runtime, submodule or not.
Data flow
A REA tool result is turned into a graph fragment, merged into the persistent graph one finding at a time, and then queried through graphify.
REA (MCP server)
|
| tool result (REA's analysis output)
v
rea_client (MCP client: spawns REA, calls its tools, yields results)
|
| raw result dict
v
rea_adapter.to_fragment (REA result -> node-link / JAG fragment)
|
| fragment dict
v
bridge.ingest_stream (per finding: normalize -> merge -> SAVE)
| crash-safe (saved after every merge)
| fail-open (one bad finding is skipped, not fatal)
v
reagraph.json (persistent memory, graphify node-link JSON)
|
| query / path / explain
v
graphify
The pieces in detail:
rea_clientimplements the bridge'sReaSourcecontract.ReaSourceis a Protocol with a single method,findings() -> Iterable[dict]. The client's job is to spawn REA (perREAGRAPH_REA_CMD, defaultnpx -y rea-agents@latest), drive its analysis tools over MCP, and yield one result dict per finding. Because the contract is just "yield dicts", REAgraph never imports REA and is never coupled to it at the code level.rea_adapter.to_fragmentmaps one REA tool result into the fragment shape the bridge expects: either a JAG document (nodes and edges carrying anepistemicstate) or an already node-link document. The adapter is the one place that knows REA's result shape; everything downstream is REA-agnostic.bridge.ingest_streamtakes a source (or any iterable of dicts), and for each finding: normalizes it to a node-link fragment, merges it into the on-disk graph, and saves immediately. JAG epistemic states map to graphify confidence (observedbecomesEXTRACTED,inferredbecomesINFERRED,unavailableedges are dropped), and the original relation is preserved incontextasrea:<relation>so provenance is never lost. Saving after every merge is what makes the stream crash-safe: if the run dies on finding N, findings 1 through N-1 are already durably on disk. Skipping a malformed finding (logged to stderr, never raised) is what makes it fail-open: one bad symbol never discards the whole analysis.reagraph.jsonis the persistent graph, written in graphify's node-link JSON. Node identity is the nodeid; edge identity is the tuple(source, target, relation). Identity is never a byte offset, so re-observing a thing dedups and upgrades its confidence rather than duplicating it.- graphify reads that same file for query, path, and explain, for example
graphify query "where is the license checked" --graph reagraph.json.
How to use it
The live path, once the REA client is wired, is: point REAgraph at REA and let it stream findings into the graph. REA is spawned on demand, so there is nothing to start by hand.
- Default command:
npx -y rea-agents@latest. This fetches and runs REA's MCP server, so a recent Node toolchain is the only prerequisite for the static analyses. - Override: set
REAGRAPH_REA_CMDto run a different REA (pinned version, local checkout, container). REAgraph runs exactly that command and speaks MCP to it.
Which analyses need a native engine, and which do not:
- No native engine required. Static analysis of JavaScript, TypeScript, and
Electron applications, and other targets where symbols and strings survive
(for example .NET and WASM). These are text-like artifacts that REA can reason
over without a disassembler, so the default
npxREA is enough. - Native engine required. Compiled native binaries (stripped or otherwise) that need a disassembler or decompiler. REA drives one of Ghidra, Hopper, or IDA for these. That engine is REA's own dependency: you install and configure it the way REA documents, and REAgraph inherits whatever REA can do once it is set up. REAgraph adds no native analysis of its own.
Graceful degradation when REA or MCP is absent. REAgraph is useful without a live REA at all:
- Offline
reagraph add. If you already have REA findings on disk (JAG JSON, or any node-link graph), fold them into memory directly:reagraph add finding.jag.json. This path uses only theingestandmemorymodules and needs neither REA nor themcppackage.reagraph diff,stats,summary, andcoverageare all offline too. - A fake source for the live loop without REA. The bridge consumes any object
satisfying the
ReaSourcecontract, so a fake in-memory source can exercise the whole ingest loop (merge, save, dedup, confidence upgrade, fail-open skip) without spawning REA. The test suite drives the bridge this way (tests/test_bridge.py), and a fake server underexamples/serves the same purpose for trying the integration locally.bridge.FileReaSourceis the built-in version of this: give it a list of finding files and it yields them one per file, skipping any that fail to load. - Missing
mcp. Themcppackage is only needed to run or speak to an MCP server. It is lazy-imported, so everything above (offline add, diff, the fake source) works whether or notmcpis installed. Onlyreagraph serveand the live REA client actually require it.
Limitations
This integration is deliberately honest about what it does not guarantee.
- REA's output format moves fast. REA is actively developed and its result shape
changes.
rea_adapter.to_fragmentis therefore best-effort and fail-open: it maps the fields it recognizes and the bridge skips (with a stderr note) any finding it cannot normalize, rather than aborting the run. A REA release can still change a field the adapter has not caught up to; when that happens you get skipped findings and a log line, not a crash, and the adapter is the one place to update. - Native analysis inherits REA's setup requirements. Anything that needs Ghidra, Hopper, or IDA is exactly as capable, and exactly as fiddly to install, as REA itself. REAgraph does not package, bundle, or simplify those engines; if REA cannot analyze a given native binary in your environment, neither can REAgraph.
- Privacy and legal responsibility is yours and unchanged. REA may send analysis work to a model provider, so results and artifact details can leave your machine; that is a property of running REA, and REAgraph routing REA's output into a local graph does not change it. You are responsible for having authorization to analyze whatever target you point this at. REAgraph adds memory, structure, and diff on top of REA; it adds no new permission to analyze anything and makes no legal or privacy guarantee about what you choose to run.
REAgraph is an independent project and is not affiliated with or endorsed by the REA project.