open_binary binds one target to one provider without a Ghidra import, and the first Ghidra-backed call pays for a headless import, auto-analysis, and a private socket.
Your agent opened ledger-arm64-O1 over MCP in under two seconds and then waited several seconds for list_procedures. Nothing was wrong. The open call did not import the file into Ghidra. The first question did, and every question after it came back in milliseconds.
When you finish this section, you can read the server log of a session and name the process that the first query started. You can also predict which calls will be slow.
open_binary
"method": "tools/call",
"params": {
"name": "open_binary",
"arguments": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"provider_id": "ghidra"
}
}
…
"structuredContent": {
"result": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"format": "mach-o",
"kind": "executable",
"sha256": "01b5d08b…",
"architecture": "arm64"
}
}Original source
title: the open_binary exchange
source: capture/out/10-mcp-03-open-binary-O1.json
lang: json
note: The request and the result. The text copy of the result is cut by the capture kit.
---
"method": "tools/call",
"params": {
"name": "open_binary",
"arguments": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"provider_id": "ghidra"
}
}
…
"structuredContent": {
"result": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"format": "mach-o",
"kind": "executable",
"sha256": "01b5d08b…",
"architecture": "arm64"
}
}The handler calls session.open with the path and the provider id, then returns five fields: path, format, kind, sha256, and architecture. To fill them rea reads the magic bytes: 7f 45 4c 46 for ELF, MZ for DOS and PE, and four Mach-O values. It hashes the whole file in 64 KiB chunks. A file with no header needs an explicit format. With format: "raw-binary" you also give architecture and entry_point, and the result adds entry_point. rea refuses a WebAssembly module here and sends it to inspect_wasm_artifact. For every other target, rea selects the provider you asked for, and binary_session records that choice.
The selection waits for the provider's readiness checks. On the capture Mac, the server log shows about 1.6 s for the first open_binary and about 0.4 s for the second. The design docs allow the wait: a readiness check can wait for the commands that it owns. Release 6.5.0 answered the same calls in 36 ms and 1 ms. Neither version imports the target at this step.
"open": true,
…
"analysis_provider_binding": {
"provider": {
"id": "ghidra",
"name": "Ghidra",
"version": "12.1.4"
},
"selection_source": "request",
"analysis_profile": {
…
"digest": "3fc72c9b…"
}
},
…
"server_identity": {
"package": {
"name": "rea-agents",
"version": "6.7.0",
…
},
…
"sdk": {
"server": "2.3.1",
…
},
"negotiated_protocol_version": "2025-06-18",
…Original source
title: the binding that binary_session reports
source: capture/out/10-mcp-04-binary-session.json
lang: json
note: Cut to the provider binding and the server identity. The capabilities, candidates, and tool availability lists are cut.
---
"open": true,
…
"analysis_provider_binding": {
"provider": {
"id": "ghidra",
"name": "Ghidra",
"version": "12.1.4"
},
"selection_source": "request",
"analysis_profile": {
…
"digest": "3fc72c9b…"
}
},
…
"server_identity": {
"package": {
"name": "rea-agents",
"version": "6.7.0",
…
},
…
"sdk": {
"server": "2.3.1",
…
},
"negotiated_protocol_version": "2025-06-18",
…selection_source: "request" says the agent chose Ghidra. The profile digest is the one every Ghidra record of this target carries. The docs state the limit of the open call in one sentence:
Original source
label: what open_binary does not do
source: mcp §Ghidra first-query deadlines and recovery
---
`open_binary` picks the target and binds the provider. It does not prove that Ghidra has finished the import of the target.The first Ghidra-backed call
03 tools/call open_binary ok -> 10-mcp-03-open-binary-O1.json (…)
04 tools/call binary_session ok -> 10-mcp-04-binary-session.json (…)
05 tools/call list_procedures ok -> 10-mcp-05-list-procedures.json (…)
06 tools/call procedure_assembly ok -> 10-mcp-06-procedure-assembly-grade.json (…)
07 tools/call xrefs ok -> 10-mcp-07-xrefs-grade.json (…)
08 tools/call list_strings ok -> 10-mcp-08-list-strings.json (…)Original source
title: six calls from the session index
source: capture/out/10-mcp-session.txt
lang: text
note: Lines 03 to 08 of the index. The time in parentheses is measured by the capture client around each call, changes on every run, and is cut.
---
03 tools/call open_binary ok -> 10-mcp-03-open-binary-O1.json (…)
04 tools/call binary_session ok -> 10-mcp-04-binary-session.json (…)
05 tools/call list_procedures ok -> 10-mcp-05-list-procedures.json (…)
06 tools/call procedure_assembly ok -> 10-mcp-06-procedure-assembly-grade.json (…)
07 tools/call xrefs ok -> 10-mcp-07-xrefs-grade.json (…)
08 tools/call list_strings ok -> 10-mcp-08-list-strings.json (…){"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"open_binary","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_procedures","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"procedure_assembly","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"xrefs","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_strings","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}Original source
title: the same calls in the server log
source: capture/out/10-mcp-server.log
lang: jsonl
note: The first five lines of the stderr of rea mcp. durationMs is measured inside the server. The time and durationMs values change on every run and are cut.
---
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"open_binary","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_procedures","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"procedure_assembly","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"xrefs","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_strings","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}The capture kit records the pattern in its README. The first Ghidra-backed call after open_binary takes 5 to 7 s. Every later call on the same target takes under 100 ms. The difference is the Ghidra start. GhidraClient.callTool awaits start() before it queues any request, so the first call pays for the whole sequence in #startWithin:
- Create a private runtime root named
rea-ghidra-and a random suffix under the session root. - Allocate the endpoint, a Unix socket on macOS and Linux.
- Copy the target into the root as
target-01b5d08b08fb.bin, hashing while copying, and refuse a digest mismatch. - Mint a 32-byte random token and a run id.
- Write
session.jsonwithtransport,endpoint_path,token,run_id,target_sha256,provider_version, andprofile_digest. - Start Java and wait for the bridge to accept a connection.
Headless import: a Ghidra run with no window that imports one file into a project and runs the default analyzers. On macOS and Linux, rea does not run the analyzeHeadless shell script. It asks Ghidra's LaunchSupport.jar for the environment and the JVM arguments, then runs java itself. The heap is -Xmx2G unless GHIDRA_HEADLESS_MAXMEM or GHIDRA_MAXMEM is set. The command adds -Djava.awt.headless=true and names the class ghidra.Ghidra with ghidra.app.util.headless.AnalyzeHeadless.
The docs give the reason for the direct start. On Apple platforms the shell wrappers hide their environment from ownership checks. With the wrappers in place, rea could not cancel the process group safely during startup {{install §Ghidra}}.
The headless arguments come in one fixed order:
- the project root and the project name
rea-project -importwith the copied target-readOnlyand-deleteProject-logand-scriptloginto the runtime root, and-scriptPathat the bridge directory-postScript ReaGhidraNoReturnFix.java-postScript ReaGhidraBridge.javawith the path ofsession.json, last
Some targets and settings need more arguments. A raw binary, a DOS file, or a language override adds a loader and a processor. Seeds and disabled analyzers add pre-scripts. The fixtures of this manual need none of them.
Original source
id: fig-4-1
kind: sequence
title: one open and three questions over MCP
claim: The open call returns after the readiness checks with no import, the first question waits seconds for the headless import, and the next takes under 100 ms.
caption: Time runs down. Solid arrows are calls, dashed arrows are replies. The violet lanes are rea's Ghidra client and the Java bridge inside analyzeHeadless. The times are the ranges that the kit notes record. From capture/out/10-mcp-server.log and 10-mcp-session.txt.The bridge on the Java side
ReaGhidraBridge.java is a Ghidra script that runs after the import and the analyzers. Its run() reads session.json and deletes it, then requires an imported program. It checks that the Ghidra version equals provider_version and compares the program's own SHA-256 with target_sha256 in constant time. It then ends the script transaction, initializes the decompiler, and serves. On a Unix socket it binds the path, sets the mode to rw-------, and accepts one client.
Each request is one JSON line, { id, token, method, params }. Each reply is { id, ok: true, result } or { id, ok: false, error: { code, message } }, and rea parses it with a strict schema. list_procedures was request number 2 on that socket, after the ping of the handshake, and it took the Java side a few milliseconds. The seconds before it were the import and the analyzers.
Original source
id: fig-4-2
kind: tree
title: the private runtime root of one session
claim: One session owns one directory with the project, the hashed target copy, the descriptor, the socket, and the logs, and close_binary deletes it.
caption: Read top to bottom. Each entry names the file or directory and what writes it. From src/ghidra/GhidraLauncher.ts and src/ghidra/GhidraClient.ts at rea 6.7.0, and capture/out/05-analyze-arm64-O1.json for the document name.Deadlines and the close
rea allows 330,000 ms for the start, set by REA_GHIDRA_STARTUP_TIMEOUT_MS. On the capture Mac the start took 5 to 7 s, and the client limit is the one to watch. The pinned client SDK 2.3.1 waits 60,000 ms by default, so it can cancel before the REA startup deadline {{mcp §Ghidra first-query deadlines and recovery}}.
The docs record about 64 s on a Linux fixture and about 163 s on Windows. Set the client deadline from your own first run on your own machine.
close_binary takes a fraction of a second in the log. The client sends shutdown to the bridge with a 10 s limit, the bridge disposes the decompiler, and -deleteProject removes the project. The next open_binary on ledger-arm64-O2 took about 0.4 s. Its first analyze_function again took seconds, because every new provider session imports the target again into a new temporary project {{mcp §Ghidra first-query deadlines and recovery}}.
Original source
- Expect the first Ghidra-backed call after `open_binary` to take seconds and the rest milliseconds.
- Set the MCP client deadline from a measured cold start on your own machine.
- Ask every question you have before `close_binary`, because the project is deleted.
- Read `durationMs` in the server log to separate rea time from Ghidra time.Sources: capture/out/10-mcp-03-open-binary-O1.json, capture/out/10-mcp-04-binary-session.json, capture/out/10-mcp-session.txt, capture/out/10-mcp-server.log, capture/out/05-analyze-arm64-O1.json, capture/README.md; rea 6.7.0 at 27dc42d: src/server/registerSessionTools.ts, src/contracts/sessionLifecycleInputs.ts, src/domain/binaryTarget.ts, src/application/BinaryTargetResolver.ts, src/ghidra/GhidraClient.ts, src/ghidra/GhidraTransport.ts, src/ghidra/GhidraTargetSnapshot.ts, src/ghidra/GhidraLauncher.ts, src/ghidra/GhidraJavaLaunch.ts, src/ghidra/GhidraClientWire.ts, src/ghidra/protocol.ts, src/config/providerStartupTimeout.ts, src/application/binary/AnalysisProviderRegistry.ts, bridge/ghidra/ReaGhidraBridge.java; rea docs/mcp-contracts.md, rea docs/installation.md, rea docs/tool-design.md; the 6.5.0 recording of capture/out/10-mcp-server.log in commit 99d70723c of this repository