mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
docs: document frictionless binary workflow
This commit is contained in:
1 parent
8ca3db829f
commit
ee6d539a40
2 files changed
+100
-150
No files matched your search
@@ -1,177 +1,70 @@
|
||||
# betterBinaryMCP
|
||||
# Better Binary
|
||||
|
||||
A TypeScript MCP server for [Hopper Disassembler](https://www.hopperapp.com/). It preserves 31 Hopper operations and adds 8 reverse-engineering workflows for Swift, Objective-C, decompilation, call graphs, cross-references, and binary summaries.
|
||||
Agent-ready Hopper Disassembler tooling: a beta.3 MCP server with 31 official proxies, 8 enhanced reverse-engineering workflows, and 3 binary-session tools.
|
||||
|
||||
## Prerequisites
|
||||
## Install from this checkout
|
||||
|
||||
Check these before you start:
|
||||
|
||||
- **Node.js 20+** (`node --version`)
|
||||
- **Hopper Disassembler 6+** on macOS, at `/Applications/Hopper Disassembler.app/`
|
||||
- **A binary to analyze** (Mach-O executable, `.dylib`, `.hop` database, etc.)
|
||||
|
||||
Accessibility permission is not required.
|
||||
|
||||
## Quick Start
|
||||
Requires macOS 12+ and Node.js 22+.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/morluto/betterBinaryMCP.git && cd betterBinaryMCP
|
||||
npm ci && npm run build
|
||||
npm ci
|
||||
npm run build
|
||||
npm link
|
||||
better-binary setup --yes
|
||||
```
|
||||
|
||||
Generate an MCP config with absolute paths filled in:
|
||||
`setup` is safe to repeat. It checks the host, installs Homebrew when approved and absent, installs Hopper from the official `hopper-disassembler` cask, and finishes with structured diagnostics. It never bundles Hopper or publishes this private package.
|
||||
|
||||
Useful onboarding commands:
|
||||
|
||||
```bash
|
||||
npm run config:print -- /path/to/your/binary
|
||||
better-binary --help
|
||||
better-binary --llms
|
||||
better-binary doctor --target /path/to/binary
|
||||
better-binary skills add
|
||||
better-binary mcp add
|
||||
```
|
||||
|
||||
Paste the output into your client's MCP config file:
|
||||
## MCP workflow
|
||||
|
||||
| Client | Config path |
|
||||
| ----------------- | ------------------------------------------------------------------------- |
|
||||
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
|
||||
| Cursor | `.cursor/mcp.json` in your project root |
|
||||
| Other MCP clients | Check the client's docs for MCP server config |
|
||||
Start `better-binary-mcp` with no target. In the connected agent:
|
||||
|
||||
Restart the client. The 39 tools appear in the tool list.
|
||||
1. Call `open_binary` with any readable local binary or `.hop` path.
|
||||
2. Call `binary_overview`, then use decompilation, strings, symbols, and xrefs tools.
|
||||
3. Call `open_binary` again to switch targets; a failed switch preserves the current target.
|
||||
4. Call `close_binary` when finished. `binary_session` reports current state.
|
||||
|
||||
```bash
|
||||
# For a .hop database:
|
||||
npm run config:print -- /path/to/file.hop --kind database
|
||||
Relative target paths resolve against the MCP server's working directory. Mach-O/FAT, ELF, PE, and Hopper databases are detected automatically. FAT loading selects the host-compatible architecture.
|
||||
|
||||
# For a FAT binary that needs loader args:
|
||||
npm run config:print -- /path/to/binary --loader-args '["-l","Mach-O","--aarch64"]'
|
||||
```
|
||||
`HOPPER_TARGET_PATH` remains supported for older configurations but is optional. `HOPPER_LAUNCHER_PATH`, `HOPPER_TARGET_KIND`, and `HOPPER_LOADER_ARGS_JSON` retain their existing meanings for legacy startup configuration.
|
||||
|
||||
### What happens on first tool call
|
||||
|
||||
The server does not launch Hopper at startup. Hopper launches lazily on the first tool call that needs it, which means the first call can take 10-30 seconds while Hopper opens and analyzes the binary. Subsequent calls are fast. If the first call times out, see [Troubleshooting](#troubleshooting).
|
||||
|
||||
### Try it
|
||||
|
||||
Once connected, call `binary_overview` with no arguments. It returns segment layout, procedure count, and string count. If you get JSON back, the server is working.
|
||||
|
||||
### Set up with your agent
|
||||
|
||||
Paste this prompt into Claude, Cursor, or any MCP-capable agent to have it do the setup for you:
|
||||
|
||||
> I want to connect betterBinaryMCP (a Hopper Disassembler MCP server) to this client.
|
||||
>
|
||||
> 1. Clone `https://github.com/morluto/betterBinaryMCP.git` and run `npm ci && npm run build`.
|
||||
> 2. Run `npm run config:print -- /path/to/my/binary` to generate the MCP server config.
|
||||
> 3. Paste the output into this client's MCP config and restart.
|
||||
> 4. Call `binary_overview` with no arguments to verify the server is working.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
MCP client ← stdio → TypeScript MCP server ← Unix socket → owned Python bridge → Hopper.app
|
||||
```
|
||||
|
||||
The codebase is a layered ESM TypeScript application. Dependencies flow inward: `domain` ← `contracts` ← `hopper` ← `application` ← `server` ← `main`.
|
||||
|
||||
- `src/main.ts` is the composition root: it parses config, wires the Hopper client, starts the stdio MCP transport, and owns process-lifetime shutdown.
|
||||
- `src/hopper/` lazily launches Hopper on the first Hopper-dependent tool and correlates bounded bridge messages, so lengthy analysis does not block the MCP handshake.
|
||||
- `bridge/hopper_bridge.py` runs inside Hopper and calls its public Python API. Hopper's bundled MCP server is not executed or required.
|
||||
- `src/application/` composes official operations into the 8 enhanced tools.
|
||||
- `src/domain/` holds pure parsers and the tagged error algebra; `src/contracts/` declares the caller-visible tool schemas.
|
||||
|
||||
## Manual MCP Configuration
|
||||
|
||||
If you prefer to write the config by hand, build the project and use absolute paths:
|
||||
Manual target-free MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"betterBinaryMCP": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/betterBinaryMCP/dist/main.js"],
|
||||
"env": {
|
||||
"HOPPER_TARGET_PATH": "/absolute/path/to/binary"
|
||||
}
|
||||
"better-binary": {
|
||||
"command": "better-binary-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Stdout is reserved for MCP protocol messages; diagnostics use stderr.
|
||||
The first `open_binary` can take time while Hopper analyzes the file. Analysis calls made without an open target return an actionable `NoBinaryOpenError`.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
| ------------------------- | -------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `HOPPER_TARGET_PATH` | yes | — | Absolute path to the binary or `.hop` database to analyze. |
|
||||
| `HOPPER_LAUNCHER_PATH` | no | `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` | Override the Hopper executable path. |
|
||||
| `HOPPER_TARGET_KIND` | no | `executable` | `executable` or `database` (for `.hop` files). |
|
||||
| `HOPPER_LOADER_ARGS_JSON` | no | `[]` | JSON array of strings passed to the Hopper launcher before the target. |
|
||||
|
||||
For an Apple FAT binary, for example:
|
||||
|
||||
```json
|
||||
["-l", "FAT", "--aarch64", "-l", "Mach-O"]
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### 31 Official Tools (proxied)
|
||||
|
||||
| Tool | Description |
|
||||
| --------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `address_name` | Get the name (label) at an address |
|
||||
| `comment` / `inline_comment` | Get comments at an address |
|
||||
| `current_address` / `current_procedure` / `current_document` | Cursor state |
|
||||
| `goto_address` / `next_address` / `prev_address` | Navigation |
|
||||
| `list_segments` / `list_procedures` / `list_strings` / `list_names` / `list_documents` / `list_bookmarks` | Listing |
|
||||
| `search_procedures` / `search_strings` | Regex search |
|
||||
| `procedure_pseudo_code` / `procedure_assembly` | Decompilation |
|
||||
| `procedure_callees` / `procedure_callers` | Call relationships |
|
||||
| `procedure_info` / `procedure_address` | Procedure metadata |
|
||||
| `set_address_name` / `set_addresses_names` | Rename labels |
|
||||
| `set_comment` / `set_inline_comment` | Write comments |
|
||||
| `set_bookmark` / `unset_bookmark` | Bookmarks |
|
||||
| `set_current_document` | Switch active document |
|
||||
| `xrefs` | Cross-references |
|
||||
|
||||
### 8 Enhanced Tools
|
||||
|
||||
| Tool | Parameters | Purpose |
|
||||
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `binary_overview` | — | Summarize documents, segments, procedures, and strings |
|
||||
| `swift_classes` | `pattern?: string` | Find Swift `_TtC` class symbols |
|
||||
| `get_objc_classes` | `pattern?: string` | Find Objective-C class labels |
|
||||
| `get_objc_protocols` | — | Find Objective-C and Swift protocol labels |
|
||||
| `batch_decompile` | `addresses: string[]` (max 20) | Decompile up to 20 procedures concurrently |
|
||||
| `get_call_graph` | `address: string`, `direction?: "forward"\|"backward"`, `depth?: 1–5` | Traverse callers or callees to a bounded depth |
|
||||
| `analyze_swift_types` | — | Categorize Swift classes, structs, enums, protocols, and extensions |
|
||||
| `find_xrefs_to_name` | `name: string` | Resolve a name and retrieve cross-references |
|
||||
|
||||
## Development
|
||||
## Development and verification
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm test
|
||||
npm run check
|
||||
```
|
||||
|
||||
`npm test` builds first. Tests cover all 39 contracts, every handler through a beta.3 MCP client, bridge failures, concurrency, strict boundary parsing, and the compiled stdio runtime.
|
||||
|
||||
Run the real-Hopper verifier with a representative binary and any required loader options:
|
||||
|
||||
```bash
|
||||
HOPPER_TARGET_PATH=/path/to/binary \
|
||||
HOPPER_LOADER_ARGS_JSON='["-l","Mach-O","--aarch64"]' \
|
||||
npm pack --dry-run
|
||||
HOPPER_TARGET_PATH=/path/to/target-a \
|
||||
HOPPER_SECOND_TARGET_PATH=/path/to/distinct-target-b \
|
||||
npm run verify:hopper
|
||||
```
|
||||
|
||||
It asserts the exact 39-tool inventory, document access, `binary_overview`, segment reads, a one-procedure bounded decompile, clean protocol output (no stderr), absence of Hopper's bundled MCP process, and clean shutdown (no leaked bridge session directories or lingering processes).
|
||||
Real-Hopper verification requires Hopper and a representative target. The stdio server uses only the repository's authenticated Unix-socket bridge; Hopper's bundled MCP server is not used.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If startup times out, verify the target path and reproduce the generated loader chain with Hopper's `hopper` launcher. A visible loader or save dialog prevents the post-analysis bridge from starting; supply explicit loader/CPU options or close the stale dialog. Granting Accessibility access will not fix this.
|
||||
|
||||
`batch_decompile` accepts at most 20 addresses, and `get_call_graph` accepts depths from 1 through 5.
|
||||
The provisional package, executable, MCP, skill, and config names are centralized in `src/identity.ts` so release naming can change cleanly. The package remains private and is not ready for publication.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { readdir, realpath } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -21,15 +21,27 @@ const textValue = (result) => {
|
||||
|
||||
const jsonValue = (result) => JSON.parse(textValue(result));
|
||||
|
||||
if (!process.env.HOPPER_TARGET_PATH) {
|
||||
throw new Error("HOPPER_TARGET_PATH is required");
|
||||
}
|
||||
const targetPath = process.env.HOPPER_TARGET_PATH;
|
||||
const secondTargetPath = process.env.HOPPER_SECOND_TARGET_PATH;
|
||||
if (!targetPath || !secondTargetPath)
|
||||
throw new Error(
|
||||
"HOPPER_TARGET_PATH and HOPPER_SECOND_TARGET_PATH are required and must name distinct binaries",
|
||||
);
|
||||
const [targetA, targetB] = await Promise.all([
|
||||
realpath(targetPath),
|
||||
realpath(secondTargetPath),
|
||||
]);
|
||||
if (targetA === targetB)
|
||||
throw new Error("Real-Hopper verification requires two distinct targets");
|
||||
const serverEnvironment = { ...process.env };
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
delete serverEnvironment.HOPPER_SECOND_TARGET_PATH;
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["dist/main.js"],
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
env: serverEnvironment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderrBytes = 0;
|
||||
@@ -45,10 +57,21 @@ try {
|
||||
const expectedNames = TOOL_CONTRACTS.map(({ name }) => name).sort();
|
||||
const actualNames = listed.tools.map(({ name }) => name).sort();
|
||||
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
|
||||
throw new Error("The real server did not expose the intended 39 tools");
|
||||
throw new Error("The real server did not expose the intended 42 tools");
|
||||
}
|
||||
|
||||
const options = { timeout };
|
||||
const initialSession = await client.callTool(
|
||||
{ name: "binary_session", arguments: {} },
|
||||
options,
|
||||
);
|
||||
if (jsonValue(initialSession).open !== false)
|
||||
throw new Error("The verifier did not start without a target");
|
||||
const opened = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path: targetA } },
|
||||
options,
|
||||
);
|
||||
if (opened.isError === true) throw new Error(textValue(opened));
|
||||
const documents = await client.callTool(
|
||||
{ name: "list_documents", arguments: {} },
|
||||
options,
|
||||
@@ -74,6 +97,22 @@ try {
|
||||
{ name: "batch_decompile", arguments: { addresses: [firstAddress] } },
|
||||
options,
|
||||
);
|
||||
const switched = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path: targetB } },
|
||||
options,
|
||||
);
|
||||
if (switched.isError === true) throw new Error(textValue(switched));
|
||||
const secondSession = jsonValue(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }, options),
|
||||
);
|
||||
if (secondSession.path !== targetB)
|
||||
throw new Error("The real session did not switch to target B");
|
||||
const secondOverview = await client.callTool(
|
||||
{ name: "binary_overview", arguments: {} },
|
||||
options,
|
||||
);
|
||||
if (secondOverview.isError === true)
|
||||
throw new Error(textValue(secondOverview));
|
||||
|
||||
const processList = await execFileAsync("ps", ["-ax", "-o", "command="]);
|
||||
const bundledMcpRunning = processList.stdout
|
||||
@@ -88,6 +127,12 @@ try {
|
||||
throw new Error("The MCP runtime emitted stderr during verification");
|
||||
}
|
||||
|
||||
const closed = await client.callTool(
|
||||
{ name: "close_binary", arguments: {} },
|
||||
options,
|
||||
);
|
||||
if (closed.isError === true) throw new Error(textValue(closed));
|
||||
|
||||
summary = {
|
||||
toolCount: actualNames.length,
|
||||
documentCount: jsonValue(documents).length,
|
||||
@@ -98,6 +143,10 @@ try {
|
||||
boundedResultKeys: Object.keys(jsonValue(bounded)).length,
|
||||
bundledMcpRunning,
|
||||
stderrBytes,
|
||||
dynamicSession: true,
|
||||
targets: [targetA, targetB],
|
||||
switched: true,
|
||||
secondOverview: jsonValue(secondOverview),
|
||||
};
|
||||
} finally {
|
||||
const keepAlive = setInterval(() => undefined, 100);
|
||||
@@ -130,6 +179,14 @@ if (
|
||||
"The TypeScript MCP server remained alive after client close",
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...summary, cleanShutdown: true }, null, 2)}\n`,
|
||||
);
|
||||
if (summary === undefined)
|
||||
throw new Error("Real-Hopper verification did not produce a summary");
|
||||
await new Promise((resolve, reject) => {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...summary, cleanShutdown: true }, null, 2)}\n`,
|
||||
(cause) => {
|
||||
if (cause) reject(cause);
|
||||
else resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user