使用说明
Binary Ninja Code Mode MCP
A Model Context Protocol (MCP) server for Binary Ninja that enables LLM-assisted reverse engineering through code execution.
Overview
This plugin implements Anthropic's Code Execution pattern. Instead of accessing the typical MCP "tools", the LLM writes Python code that executes directly against Binary Ninja's API. This approach (described by Cloudflare as "Code Mode") is more token-efficient and enables more complex multi-step analyses in a single execution.
Key Features
- Write Python that runs directly against Binary Ninja's API
- Query and mutate the binary database
- Checkpoint/rollback, persistent workspace files, and reusable analysis patterns
- The API reference is generated from the code and rides in the tool description, so the model always has it and it can never drift from what the plugin actually exposes
search_api()anddescribe()read Binary Ninja's own API across 31 types, so anything the wrapper does not cover is something the model looks up rather than a dead end
Installation
Method 1: Plugin Manager (Recommended)
- In Binary Ninja, open the Plugin Manager (
Plugins > Manage Plugins) - Search for
Code Mode MCPorbinja_codemode_mcp - Click
Install - Restart Binary Ninja
After installation, the plugin will be located in the community plugins folder:
# Linux
~/.binaryninja/repositories/community/plugins/akrutsinger_binja_codemode_mcp/
# macOS
~/Library/Application Support/Binary Ninja/repositories/community/plugins/akrutsinger_binja_codemode_mcp/
# Windows
%APPDATA%\Binary Ninja\repositories\community\plugins\akrutsinger_binja_codemode_mcp\
Method 2: Manual Installation
Clone or download this repository and copy to your Binary Ninja plugins folder:
# Linux
cp -r . ~/.binaryninja/plugins/binja_codemode_mcp/
# macOS
cp -r . ~/Library/Application\ Support/Binary\ Ninja/plugins/binja_codemode_mcp/
# Windows
xcopy /E /I . "%APPDATA%\Binary Ninja\plugins\binja_codemode_mcp"
Copy the whole repository, not just plugin/: plugin/ is a subpackage and imports config.py
and plugin.json from the directory above it.
MCP Client Configuration
The plugin serves MCP over HTTP at http://127.0.0.1:42069/mcp, authenticated with a bearer token.
There is no bridge script to launch, so the configuration no longer depends on where the plugin was
installed: point your client at the URL.
Claude Code — one command, no config file to edit:
claude mcp add binja-codemode-mcp -s user --transport http \
http://127.0.0.1:42069/mcp \
--header "Authorization: Bearer binja-codemode-local"
-s userregisters the server for every project. Omit it to scope the server to the current project only, or use-s projectto write a shared.mcp.jsonyou can commit.- Verify with
claude mcp list, or/mcpinside a session. Remove withclaude mcp remove binja-codemode-mcp.
Clients configured by JSON (.mcp.json, Claude Desktop, Zed and others) take the same three
values. Check your client's docs for the exact key names; the shape is usually:
{
"mcpServers": {
"binja-codemode-mcp": {
"type": "http",
"url": "http://127.0.0.1:42069/mcp",
"headers": {
"Authorization": "Bearer binja-codemode-local"
}
}
}
}
Clients that only speak stdio can front the server with
mcp-remote:
{
"mcpServers": {
"binja-codemode-mcp": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://127.0.0.1:42069/mcp",
"--header",
"Authorization: Bearer binja-codemode-local"
]
}
}
}
Custom API Key (Optional)
To use a custom API key instead of the default API key, create
~/.binaryninja/codemode_mcp/config.json:
{
"api_key": "your-custom-key"
}
Then use the same key in your client's Authorization: Bearer ... header. Plugins > Code Mode MCP > Show API Key prints the key the running server expects.
Logging (Optional)
The server logs to Binary Ninja's own log window; there is no separate process to configure. Raise Binary Ninja's log level to see more.
Usage
- Open Binary Ninja and load a binary
- Start the server:
Plugins > Code Mode MCP > Start Server - In your MCP client (Claude, Zed, etc.), start prompting!
Start the server before connecting the client. Clients ask for the tool list once, at startup, and that response is what carries the API reference, so a client that connected first will not see the binary or the methods until it reconnects.
One server serves the one binary it was started against. Other windows and tabs are not reachable through it - their status indicator says so - and closing the served binary stops the server.
Example Prompts
"List all functions that reference memcpy and check if they validate buffer sizes"
"Decompile main() and identify potential security issues"
"Search all decompiled code for malloc calls without corresponding free"
"Find all call chains from main to a suspicious function at 0x401234"
"Create a checkpoint, then rename all sub_* functions based on their behavior"
"Get the control flow graph for the authentication function and identify loops"
"Patch the license check at 0x403000 to always return true"
"Find and categorize all string references by type (URL, file path, error message, etc.)"
"Analyze the binary's attack surface by examining input validation in network-facing functions"
API Overview
Executed code has five names in scope: binja for analysing the binary, workspace for files that
outlive a call, skills for saved code, bv for the raw BinaryView, and bn for the
binaryninja module. Workspace files are per binary — they hold results about the binary in front
of you — while skills are shared, being code meant to work on any of them. The plugin builds one
mapping of those names and renders both this list and the execution namespace from it, so what the
model is told it can call is what it can call.
binja is deliberately small. What it covers is what only it can do: state that outlives a single
call, introspection of the Binary Ninja that is actually running, and the few idioms that are
awkward to render or easy to get wrong. Listing functions, searching for bytes, reading strings and
walking a control flow graph are not there — each is a comprehension over bv, and a wrapper around
one is only somewhere for the two to disagree. Everything else is bv and bn directly, with
binja.function() to turn a name or an address into a real Function, and binja.search_api() /
binja.describe() to look up members rather than guess at them. A saved skill is code, not a
callable: exec(skills.get_code(name)) at the top level of a call defines its names into that
call's namespace.
Generated by scripts/generate_docs.py from plugin/api.py and plugin/workspace.py.
Query Operations (read-only)
binja.decompile(func: Function | str | int, il_level: str = 'hlil') -> str | None— Decompile function to C-like pseudocode.binja.get_assembly(func: Function | str | int) -> str | None— Get disassembly for function.binja.get_all_xrefs(addr: int, include_data: bool = True, include_code: bool = True) -> dict— Get all cross-references (both code and data) to/from one address, not a whole function. Returns {address, xrefs_to: [{type, from_address, from_function}], xrefs_from: [{type, to_address, to_function}]}binja.function(func: Function | str | int)— Get the Function object for a name or an address, for work these methods do not cover. Returns A binaryninja.Function, or None if nothing resolves
Mutation Operations (tracked)
binja.define_type(c_definition: str) -> list[str]— Define types from C syntax, and name the ones it defined. Returns [name, ...] for each type defined, empty when the C declared nonebinja.set_function_signature(func: Function | str | int, signature: str) -> bool— Set function prototype, and wait for the analysis that makes it visible. Returns True, or False if the function does not resolve; a signature that does not parse raises
Checkpoints
binja.checkpoint(name: str) -> bool— Name the current state of the database so a later rollback can return to it. Lasts as long as the server runs. Returns True, or False if the name is empty or already takenbinja.rollback(name: str) -> bool— Undo every change made since the named checkpoint, discarding later checkpoints. Returns True, or False if no checkpoint of that name existsbinja.delete_checkpoint(name: str) -> bool— Forget a checkpoint without undoing anything. Returns True, or False if no checkpoint of that name existsbinja.list_checkpoints() -> list[dict]— List saved checkpoints, oldest first. Returns [{name, undo_depth}, ...]
Discovery
binja.list_methods() -> str— List every method callable here, with signatures and summaries.binja.search_api(query: str, limit: int = 40) -> list[dict]— Search Binary Ninja's own API by keyword, for what the methods above do not cover. Returns [{name, kind, signature, summary}, ...] where name is what describe() acceptsbinja.describe(name: str) -> dict— Full signature and docstring for one Binary Ninja API member, class or enum. Returns {name, kind, signature, doc}, plus also_defined_on for an ambiguous bare name or members for an enum
Workspace files, for carrying results between calls
workspace.write(name: str, content: str) -> bool— Write content to workspace file.workspace.read(name: str) -> str | None— Read content from workspace file.workspace.list() -> list[dict]— List all workspace files with metadata. Returns [{name, size, modified}, ...]workspace.delete(name: str) -> bool— Delete a workspace file.workspace.clear() -> int— Clear all workspace files. Returns count deleted.
Reusable analysis code, saved across sessions
skills.save(name: str, code: str, description: str) -> bool— Save a skill.skills.load(name: str) -> Skill | None— Load a skill by name.skills.list() -> list[dict]— List all skills with descriptions. Returns [{name, description}, ...]skills.delete(name: str) -> bool— Delete a skill.skills.get_code(name: str) -> str | None— Get just the code for a skill.
Regenerating the docs
The API surface is a hand-written class, not something generated from a spec, but everything the LLM and the README say about it is generated from that class. Nothing describing the API is maintained by hand.
export PYTHONPATH=/path/to/binaryninja/python # the dir holding the `binaryninja` module
python3 scripts/check_api.py --check # every method documented? README current?
python3 scripts/generate_docs.py # rewrite the README's API section
Adding a method to BinjaAPI is enough to advertise it: check_api.py will insist it carries a
docstring summary and, if it returns a dict, a Returns: line giving the shape, since both are
rendered verbatim into the tool description. Put it under an existing # ===-fenced section comment
and it is grouped automatically.
check_api.py also prints what the tool description costs, which is worth watching - it is in the
model's context on every request.
Security
The LLM's code runs inside Binary Ninja, unsandboxed, with the same access as Binary Ninja
itself. It can read and write your files and reach the network. A plugin that takes no
dependencies has no way to sandbox Python, and bv alone reaches enough of the process that
restricting the rest would buy nothing, so the plugin does not pretend otherwise.
What actually limits exposure:
- The server binds to localhost (127.0.0.1) only
- Every request needs the API key, as an
Authorization: Bearerheader - Requests carrying a non-localhost
Originare refused, so a web page cannot reach it - Execution is capped at 30 seconds and output at ~6,000 tokens
- Mutations are tracked, so a checkpoint can be rolled back
Earlier versions advertised an AST validator that rejected import os and similar. It was not a
boundary: exec() with a globals dict lacking __builtins__ gets the real builtins injected by
CPython, so open() and every import already worked, and the check matched syntax only. It has been
removed rather than left to imply protection it never gave.
Only point this at MCP clients and models you trust, and prefer working on a copy of any binary you care about.