RE-MCP is a multi-backend reverse-engineering server that communicates over the Model Context Protocol (MCP). It uses headless APIs — idalib for IDA Pro, pyghidra for Ghidra — to expose binary analysis capabilities as structured tool calls that LLMs can invoke.
The project is a monorepo with three packages:
re-mcp-core(packages/re-mcp-core/src/re_mcp/) — generic MCP supervisor infrastructure (transport, worker management, tool transforms, sandboxed execution)re-mcp-ida(packages/re-mcp-ida/src/re_mcp_ida/) — IDA-specific backend (idalib bootstrap, tools, resources, prompts)re-mcp-ghidra(packages/re-mcp-ghidra/src/re_mcp_ghidra/) — Ghidra-specific backend (pyghidra bootstrap, tools, resources, prompts)
The server supports three transport modes:
# Default: direct stdio (single-session, workers die on disconnect)
LLM Client <──stdio──> ProxyMCP (re_mcp.supervisor)
└── WorkerPoolProvider
├──stdio──> Worker 1
└──stdio──> Worker 2
# Proxy mode: stdio proxy → persistent HTTP daemon (workers survive reconnects)
LLM Client <──stdio──> Proxy (re_mcp.proxy) <──HTTP──> Daemon (re_mcp.daemon)
│
ProxyMCP (re_mcp.supervisor)
│
└── WorkerPoolProvider
├──stdio──> Worker 1
└──stdio──> Worker 2
The default mode (<backend> or <backend> stdio) runs the supervisor directly over stdio — workers die when the client disconnects. This is the simplest mode, widely supported across MCP clients.
The proxy mode (<backend> proxy) runs a stdio-to-HTTP proxy that auto-spawns a persistent background daemon. The daemon runs ProxyMCP (from re_mcp.supervisor) over streamable HTTP with bearer token authentication, so worker processes and database state survive client reconnections. The proxy bridges stdio MCP messages bidirectionally to the daemon.
The serve mode (<backend> serve) runs the daemon directly (used by the proxy's auto-spawn, or for manual daemon management).
The stop command (<backend> stop) gracefully shuts down a running daemon.
For single-database usage, there is one worker. Multiple workers are spawned when open_database is called multiple times (the default keeps previously opened databases open).
Both idalib and pyghidra run their respective analysis engines as libraries within a normal Python process — no GUI, no X11/display dependencies. This has several advantages:
- Process lifecycle is controlled by the server, not the GUI
- Direct function calls instead of script injection
- Runs on headless machines (CI, SSH, containers)
The trade-off is that both backends are thread-affine: all API calls must happen on the same thread that initialized the engine (the idapro import for idalib, the JVM start for pyghidra). Each worker process handles a single database; the supervisor routes requests to the correct worker via stdio pipes.
FastMCP provides a decorator-based API for defining MCP tools. Each tool is a plain Python function with type annotations — FastMCP handles JSON schema generation, argument validation, and transport.
The default stdio mode is the simplest transport — stdio is widely supported across MCP clients, requires no port management or auth, and ties process lifecycle to the client session. The trade-off is that when the client disconnects (e.g. the user closes their editor), the supervisor and all worker processes die, losing database state and analysis progress.
The proxy mode solves this with a persistent HTTP daemon behind a stdio proxy.
The daemon (re_mcp.daemon) runs ProxyMCP over FastMCP's streamable HTTP transport with bearer token authentication. It writes a state file containing the host, bound port, bearer token, PID, and version. The state file location is platform-specific: ~/Library/Application Support/<backend>/daemon.json on macOS, $XDG_STATE_HOME/<backend>/daemon.json (defaulting to ~/.local/state/<backend>/daemon.json) on Linux, and %LOCALAPPDATA%\<backend>\daemon.json on Windows (where <backend> is re-mcp-ida or re-mcp-ghidra). The state file is created atomically with restricted permissions (0o600) and cleaned up on shutdown.
The proxy (re_mcp.proxy) is the entry point for <backend> proxy mode. It checks for an existing daemon via the state file and process liveness, spawns one if needed (with a file lock to prevent races between concurrent clients), and bridges stdio MCP messages to the daemon's HTTP endpoint using mcp.client.streamable_http. The daemon process is fully detached (new session on Unix, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP on Windows) so it survives the proxy's exit.
When auto-spawned by the proxy, the daemon enables idle auto-shutdown (default 300 seconds, configurable via <PREFIX>IDLE_TIMEOUT). An _idle_monitor coroutine runs alongside the uvicorn server, polling every 10 seconds. When uvicorn has no active connections, the provider has no registered MCP sessions, no worker calls are in flight, and no proxy keepalive is active — all for the idle timeout duration — the monitor sets server.should_exit = True to trigger a graceful shutdown. The provider's lifespan then terminates all workers via shutdown_all(), and the daemon's finally block removes the state file. The idle timer resets whenever activity resumes. Daemons started manually via <backend> serve do not enable idle shutdown by default (--idle-timeout=0); pass --idle-timeout=N to opt in.
Security: the bearer token is a 256-bit random hex string generated per daemon lifetime. Only the spawning proxy and the daemon know the token. The daemon binds to 127.0.0.1 by default; binding to non-loopback addresses emits a warning.
Each backend implements the Backend protocol (defined in re_mcp.backend), which provides:
info()— returns aBackendInfodataclass with the backend name, display name, URI scheme, worker module, pinned/management tool sets, environment variable prefix, and state directory name.build_instructions()— generates LLM-facing instructions describing the backend's capabilities and workflows.register_management_tools()— registers backend-specific management tools (e.g. IDA'sopen_databasewithprocessor/fat_archparameters, Ghidra's withlanguage/compiler_spec).register_prompts()— registers MCP prompt templates for guided workflows.canonical_path()— computes a dedup key for a database path, used byWorkerPoolProviderto prevent duplicate workers for the same database.list_targets()— lists available analysis targets (processors, languages, etc.) for the backend.
Backends are discovered via re_mcp.backends entry points, allowing new backends to be added as separate packages.
Both backends are thread-affine: all engine API calls must happen on the main OS thread. The MCP event loop runs on a background thread (a Python thread with daemon=True, not to be confused with the persistent HTTP daemon), while the main thread runs a MainThreadExecutor work queue.
Each backend's Server subclass (IDAServer, GhidraServer) wraps every sync tool and resource function registered via @mcp.tool() or @mcp.resource() into an async def that dispatches the call to the main thread via call_ida / call_ghidra (both aliases for dispatch_to_main, backed by MainThreadExecutor). FastMCP sees an async function and skips its own threadpool, so all engine API calls land on the main thread while the MCP server remains responsive. Async tool functions run on the event-loop thread and must use the dispatch function for individual engine API calls.
IDA-specific: Functions in re_mcp_ida.helpers that contain IDA API calls are marked with @ida_dispatch. This decorator tags the function with a _ida_dispatch attribute (it does not alter execution) and signals that the function must be invoked via call_ida from async code. A pre-commit lint script (scripts/lint_ida_threading.py) enforces this: it checks that @ida_dispatch-marked functions are not called directly from async functions without going through call_ida.
Each backend provides a bootstrap() function (in <backend>.__init__) that initializes the analysis engine before any engine-specific imports. Only worker processes call bootstrap() — the supervisor never does.
IDA: bootstrap() handles the idapro import ordering constraint (see below). The supervisor avoids calling it, which also avoids the idalib license cost.
Ghidra: bootstrap() starts the JVM via pyghidra's HeadlessPyGhidraLauncher before any Ghidra Java classes are imported.
idalib requires that import idapro happen before any ida_* module is imported. The bootstrap() function in re_mcp_ida.__init__ handles this:
# packages/re-mcp-ida/src/re_mcp_ida/server.py (worker entry point)
import re_mcp_ida
re_mcp_ida.bootstrap() # Initialize idalib before any ida_* importsbootstrap() first tries a normal import idapro. If that fails, it locates the idapro wheel from the local IDA installation and adds it to sys.path.
After bootstrap() runs, ida_* imports can be top-level in all other modules — they're guaranteed to run after idapro has initialized the IDA kernel.
Each backend has a Session class (in <backend>.session) that manages the database connection within each worker process:
session = Session() # module-level singleton (one per worker process)Key behaviors:
- Each worker process handles one database (both backends use global state that requires isolation)
session.require_openis a decorator that raisesBackendErrorif no database is open. SinceBackendErrorsubclasses FastMCP'sToolError, FastMCP automatically returnsisError=Truewith the message as text content- The worker's
main()function callssession.close(save=True)in itsfinallyblock on shutdown
IDA-specific session behaviors:
- The decorator clears IDA's cancellation flag before each call and catches
Cancelledexceptions, re-raising them asIDAError - Signal handlers:
SIGTERMraisesSystemExit(triggers shutdown cleanup and save),SIGINTfirst press sets IDA's cancellation flag / second press escalates to shutdown,SIGUSR1sets the cancellation flag without escalation (cooperative cancellation from supervisor)
The supervisor uses FastMCP's native Provider system to expose worker tools and resources through the standard provider chain, rather than overriding list_tools(), call_tool(), etc.
ProxyMCP (re_mcp.supervisor) subclasses FastMCP. It creates a WorkerPoolProvider and calls self.add_provider(worker_pool). Generic management tools (close_database, save_database, list_databases, wait_for_analysis, list_targets) are registered by the supervisor; backend-specific management tools (e.g. IDA's open_database with processor/fat_arch parameters, Ghidra's with language/compiler_spec) are registered by the backend via register_management_tools(). Prompts and the database list resource are also registered directly on the supervisor — they do not require database state and are served by FastMCP's internal local provider.
A ToolTransform (a CatalogTransform subclass defined in re_mcp.transforms) is applied at the server level. It pins a set of common analysis tools (e.g. list_functions, decompile_function, get_strings) alongside five meta-tools:
search_tools— regex discovery over all non-pinned tools.get_schema— parameter schemas and return shapes for tools by name.execute— sandboxed Python that chainsawait invokecalls for multi-step pipelines and parallel queries.batch— sequential multi-tool execution with per-item error collection and progress reporting.call— lightweight proxy for calling any tool by name, including hidden tools not in the client tool list.
Tools not in the pinned set are hidden from the tool listing but callable via call, batch, or execute.
Management tools delegate to WorkerPoolProvider methods for worker lifecycle and are session-aware: close_database delegates to close_for_session(), which atomically detaches and conditionally terminates under _lock; save_database checks attachment before proceeding. Most management tools use try_get_session_id() (from re_mcp.context) to get the session ID without exposing a ctx parameter in the tool schema. save_database is the one exception — it accepts ctx for heartbeat progress notifications during long saves (FastMCP strips ctx from the JSON schema automatically).
WorkerPoolProvider (re_mcp.worker_provider) implements FastMCP's Provider interface. It manages worker subprocesses (each via a fastmcp.Client with StdioTransport) and exposes their tools and resources through the provider chain:
_list_tools()/_get_tool()returnRoutingToolinstances —Toolsubclasses constructed from bootstrapped MCP tool schemas. EachRoutingToolhas thedatabaseparameter injected into its JSON schema at construction and preservesoutput_schemafor structured output passthrough._list_resource_templates()/_get_resource_template()returnRoutingTemplateinstances —ResourceTemplatesubclasses that override_read()to extractdatabasefrom params, resolve the worker, reconstruct the backend URI, and proxy the read viaclient.read_resource_mcp(). All worker resources (both fixed resources and templates) are exposed as templates with a{database}prefix in the URI.RoutingTool.run()popsdatabasefrom arguments, resolves the target worker, implicitly attaches the current session (if aContextis available), and delegates toproxy_to_worker()(which tracks active calls viaworker.dispatch()and callsworker.client.call_tool_mcp()). The result is enriched withdatabaseand returned as aToolResult, or raised as aToolError. Error handling (worker crashes, timeouts) is contained withinproxy_to_worker().lifespan()shuts down all workers on exit.check_attached(worker, session_id)raisesBackendErrorif the session is not attached to the worker (pass-through when session ID isNoneor the worker has no tracked sessions for backward compatibility).close_for_session(worker, session_id, save, force)atomically checks attachment, detaches, and conditionally terminates under_lock— prevents races where a concurrentattach()fromRoutingTool.run()could sneak in between detach and terminate.detach_all(session_id, terminate=True)detaches a session from all workers under_lock. Whenterminate=True(default), workers whose session set becomes empty are shut down;terminate=Falsedetaches for bookkeeping only (used by the disconnect callback). Falls back toshutdown_all()when session ID isNone.build_database_list(include_state, caller_session_id)returns all open databases with metadata and asession_countper entry; whencaller_session_idis provided, each entry also includes anattachedflag.
Tool/resource schemas are bootstrapped lazily from a temporary worker on first access. RoutingTool and RoutingTemplate both set task_config = TaskConfig(mode="optional").
All tools except management tools (open_database, close_database, save_database, list_databases, wait_for_analysis, list_targets) require the database parameter (the stem ID returned by open_database or list_databases).
spawn_worker() returns immediately. The worker subprocess, database open, and optional auto-analysis all run in a background asyncio.Task (_background_spawn). The initial response includes "opening": true, and callers must call wait_for_analysis to block until the database is ready for tool calls.
When run_auto_analysis=True, _background_spawn chains into a second task (via Worker.start_analysis()) that dispatches wait_for_analysis through the normal proxy path once the open completes. While that task runs, list_databases reports "analyzing": true for the worker.
While background analysis is running, every worker tool except wait_for_analysis is rejected by RoutingTool.run() — the analysis engine is occupied. wait_for_analysis awaits the background task directly rather than making a redundant proxy call.
After analysis completes, worker metadata (function count, etc.) is refreshed via get_database_info, and MCP log and resource-list-changed notifications are sent if an mcp_session is available. Clients should call wait_for_analysis on the database to block until completion rather than polling list_databases. Background spawn and analysis tasks are cancelled during worker shutdown.
Mach-O universal ("fat") binaries contain multiple architecture slices. Before spawning a worker, open_database calls check_fat_binary (in re_mcp_ida.exceptions), which parses the on-disk FAT_MAGIC / FAT_MAGIC_64 header via detect_fat_slices and refuses to proceed without an explicit fat_arch parameter — headless idalib would otherwise silently pick a default slice. The resulting IDAError uses error_type="AmbiguousFatBinary" and includes an available detail listing the slice names (x86_64, arm64, arm64e, ...).
check_fat_binary returns the slice's 1-based position in the on-disk fat header, which build_ida_args emits as -T"Fat Mach-O file, <index>" — the only documented way to pick a slice in headless mode. Because the fat-slice selector already uses -T, loader cannot be combined with fat_arch.
Per-slice sidecars are stored at <binary>.<arch>.i64 (via an -o<stem> override in Session.open) so multiple architectures of the same universal binary coexist on disk. The check short-circuits on existing .i64/.idb databases (and on matching per-slice sidecars unless force_new=True), since stored analysis already pins a slice. To analyze multiple slices from the same file concurrently, open once per slice with distinct database_id values.
Because both backends use single-threaded or global-state analysis engines, requests to the same worker are serialized by the worker's single-threaded MCP transport. The dispatch() async context manager tracks active call count and activity timestamps. Requests to different workers run fully in parallel. The Worker.state property derives the effective state (BUSY/IDLE) from the _active_calls counter rather than requiring manual state transitions.
Crashed workers are detected on-demand when tool calls or resource reads encounter connection errors (ClosedResourceError, EndOfStream, BrokenPipeError/OSError, McpError with connection-closed code) — proxy_to_worker() and RoutingTemplate._read() call mark_worker_dead() to clean up.
Workers track which MCP sessions are using them via attach(session_id) / detach(session_id) / is_attached(session_id) / session_count. Sessions are attached implicitly when a tool or resource is accessed (via attach_current_session(), called from RoutingTool.run() and RoutingTemplate._read()) and explicitly on open_database.
close_database delegates to close_for_session(), which atomically checks attachment, detaches, and conditionally terminates under _lock. When other sessions are still using the database, it returns a detached status instead of terminating. save_database and close_database check attachment before proceeding (unless force=True).
When an MCP session disconnects, a cleanup callback registered on the session's exit stack automatically detaches the session from all workers (via detach_all(terminate=False)). Workers are not terminated on disconnect — in Claude Code's multi-agent architecture all agents share one MCP session, so a session cycle would otherwise kill databases still in active use. Termination happens only via an explicit close_database call, open_database(keep_open=False), or supervisor shutdown.
Each backend uses its own environment variable prefix (IDA_MCP_ for IDA, GHIDRA_MCP_ for Ghidra). The table below uses <PREFIX> as a placeholder.
<PREFIX>MAX_WORKERS— maximum simultaneous databases (clamped to 1-8 when set; unlimited when unset)<PREFIX>LOG_LEVEL— logging level (DEBUG,INFO,WARNING,ERROR,CRITICAL); defaults toWARNING, output goes to stderr<PREFIX>LOG_DIR— directory that receives per-run log files. When set, each component tees Python logging to<dir>/<run_id>-<label>.log(labels:daemon,proxy,supervisorfor direct stdio mode), each worker to<dir>/<run_id>-worker-<db>.log, and each worker's raw stderr is captured to<dir>/<run_id>-worker-<db>.stderr(catches pre-logging output and C-level crashes).<run_id>is a timestamp generated once per supervisor start. When unset, logs go only to stderr (inherited by workers).<PREFIX>IDLE_TIMEOUT— idle auto-shutdown timeout in seconds for auto-spawned daemons (default300). Set to0to disable. When the daemon has no active HTTP connections, no MCP sessions, no in-flight worker calls, and no proxy keepalive for this duration, it shuts down gracefully. Only affects daemons spawned by the proxy;<backend> servedefaults to0(use--idle-timeout=Nto override)<PREFIX>DISABLE_EXECUTE— hides theexecutemeta-tool (sandboxed Python code mode) when set to1,true,yes, oron<PREFIX>DISABLE_BATCH— hides thebatchmeta-tool when set to1,true,yes, oron<PREFIX>DISABLE_TOOL_SEARCH— disables server-side progressive tool disclosure when set to1,true,yes, oron. All tools become directly visible and callable;search_toolsandget_schemameta-tools are removed. Useful with clients that provide their own tool deferral (e.g. Claude Code).
Backend-specific:
IDADIR— path to the IDA Pro installation directory (auto-detected when unset)GHIDRA_INSTALL_DIR— path to the Ghidra installation directory (auto-detected when unset)IDA_MCP_ALLOW_SCRIPTS— enables therun_scripttool for arbitrary IDAPython execution (set to1,true, oryes)
Tools return Pydantic model instances on success (FastMCP serializes these automatically). On failure, they raise a BackendError subclass (IDAError or GhidraError). BackendError subclasses FastMCP's ToolError, so FastMCP catches it and returns isError=True with the error text as content — tools never return error dicts directly.
BackendError.__str__ returns a JSON object with error, error_type, and optional detail fields (e.g. available_variables, valid_types). This keeps the MCP error text machine-parseable while preserving a structured error taxonomy. Common error types include:
NoDatabase— no database is openInvalidAddress— could not parse/resolve addressNotFound— function, type, or symbol not foundDecompilationFailed— decompilation errorInvalidArgument— bad parameter valueCancelled— operation cancelled via cooperative cancellation
IDA-specific error types (in re_mcp_ida.exceptions):
AmbiguousProcessor— raw binary opened with a bitness-ambiguous processor module (e.g. barearm); fix by passing a variant likearm:ARMv7-MAmbiguousFatBinary— Mach-O universal binary opened withoutfat_arch; the error'savailabledetail lists the slicesUnknownFatArch—fat_archvalue not present in the fat binary; the error'savailabledetail lists the valid slicesDuplicateFatSlice— fat binary contains two slices that resolve to the same lipo-style architecture name; runlipo -thinto extract the intended slice and reopen the thin file
Individual tools define additional error types specific to their domain (e.g. ParseError, DecodeFailed, SetCommentFailed).
Mutation tools return the previous state of modified items (e.g. old_comment, old_type, old_bytes, old_flags) alongside the new values, enabling undo tracking and change verification by the LLM.
Addresses are the most common parameter type. The parse_address function in backend helpers accepts multiple formats to minimize friction for LLM callers. Resolution order:
"0x401000"— hex with0xprefix (unambiguous, tried first)"4198400"— decimal (all-digit strings are always decimal)"main"— symbol name (resolved via the backend's name database)"4010a0"— bare hex fallback (last resort; reached only when the string is not pure digits and is not a known symbol)
Symbol names are checked before bare hex so that names like add, dead, or cafe resolve to the named symbol rather than being parsed as hexadecimal. Use the 0x prefix for explicit hex (e.g. 0xADD instead of add).
Higher-level helpers build on this. Both backends provide:
resolve_address(addr)→int(raisesBackendErroron failure)resolve_function(addr)→ function object (raisesBackendError)
The IDA backend adds additional resolution helpers:
decompile_at(addr)→ decompilation result (raisesIDAError)decode_insn_at(ea)→ instruction (raisesIDAError)resolve_segment(addr)→ segment object (raisesIDAError)resolve_struct(name)→ struct ID (raisesIDAError)resolve_enum(name)→ enum ID (raisesIDAError)
Each raises the backend's error type on failure, so tool implementations avoid manual error-checking.
List-returning tools use paginate(items, offset, limit), paginate_iter(items, offset, limit), or async_paginate_iter(items, offset, limit) from the helpers module. paginate_iter consumes a generator one item at a time rather than building a full list; after collecting the requested page it reads ahead up to _COUNT_AHEAD items beyond the page end to compute has_more and an approximate total — if the iterator has more items beyond that budget, total reflects items seen so far and has_more is True. async_paginate_iter dispatches the entire iteration to the main thread (required by both backends' thread-affine engines). Tools that build lists eagerly use paginate instead. All three produce the same response shape:
{
"items": [...],
"total": 1500,
"offset": 0,
"limit": 100,
"has_more": True
}The default limit is 100 for most tools. Some tools use smaller defaults: 50 for batch decompilation/disassembly export and segment listing, 20 for find_code_by_string. There is no hard cap — callers can request larger pages when needed.
| Module | Role |
|---|---|
supervisor.py |
Main entry point — creates ProxyMCP(FastMCP) with WorkerPoolProvider, registers generic management tools. CLI dispatches to direct stdio (default), proxy, daemon, or stop mode |
daemon.py |
Persistent streamable HTTP daemon — runs ProxyMCP with bearer token auth and state file for proxy discovery. Workers survive client reconnections |
proxy.py |
Stdio-to-HTTP bridge — auto-spawns the daemon if needed, then forwards MCP messages bidirectionally between stdio and the daemon's HTTP endpoint |
worker_provider.py |
WorkerPoolProvider(Provider) — manages worker subprocesses, exposes tools via RoutingTool(Tool) and resources via RoutingTemplate(ResourceTemplate) through the native provider chain |
backend.py |
Backend protocol and BackendInfo dataclass — each backend (IDA, Ghidra, ...) implements this and registers via re_mcp.backends entry points |
context.py |
try_get_context(), try_get_session_id(), and notify_resources_changed() — FastMCP context helpers with no backend dependencies, used by supervisor modules |
exceptions.py |
BackendError(ToolError) — base structured error type for all backends |
server.py |
MainThreadExecutor work queue and BackendServer(FastMCP) — subclassed by each backend to dispatch sync tools/resources to the main thread |
helpers.py |
Backend-agnostic utilities — address parsing/formatting, pagination (paginate, paginate_iter, async_paginate_iter), dispatch_to_main main-thread dispatch, MCP annotation presets, Annotated parameter type aliases (Address, Offset, Limit, FilterPattern, HexBytes), filter compilation |
models.py |
Shared Pydantic models (e.g. PaginatedResult) used across backends |
sandbox.py |
RestrictedPythonSandbox — AST-restricted Python execution for the execute meta-tool |
transforms.py |
ToolTransform(CatalogTransform) — pins common tools, adds search_tools, get_schema, execute, batch, and call meta-tools, hides the rest from listing (callable via call/batch/execute) |
_process.py |
Platform-aware process utilities (pid_alive, pid_exit_code, IS_WINDOWS) — stdlib only, no backend dependencies |
__init__.py |
configure_logging(), ensure_run_id(), resolve_log_file(), get_version() — shared infrastructure utilities |
| Module | Role |
|---|---|
backend.py |
IDABackend — implements the Backend protocol; registers open_database, prompts, IDA-specific LLM instructions, and target listing |
server.py |
Worker entry point (re-mcp-ida-worker) — creates IDAServer (a BackendServer subclass), auto-discovers and registers all tool modules from tools/, runs stdio transport |
session.py |
Database session singleton (per worker), require_open decorator |
exceptions.py |
IDAError(BackendError) — IDA-specific structured error type, plus idalib-safe validation utilities (build_ida_args, check_processor_ambiguity, check_fat_binary, detect_fat_slices, slice_sidecar_stem, AMBIGUOUS_PROCESSORS, PRIMARY_IDB_EXTENSIONS) |
helpers.py |
Address parsing, formatting, pagination, resolution helpers, string decoding, MCP annotation presets, meta presets, Annotated parameter type aliases, call_ida main-thread dispatch, @ida_dispatch marker |
models.py |
Re-exports shared Pydantic models from re_mcp.models (e.g. FunctionSummary, RenameResult, PaginatedResult) for convenient single-source imports. Tool-specific models live in their respective tool modules; FastMCP derives the JSON output schema from each tool's return type annotation |
transforms.py |
IDA-specific tool visibility constants — PINNED_TOOLS and MANAGEMENT_TOOLS frozensets |
resources.py |
MCP resources — read-only, cacheable context endpoints (static binary data + aggregate statistics) |
prompts/ |
MCP prompt templates for guided analysis workflows (analysis, security, workflow) |
__init__.py |
Lazy bootstrap() to initialize idapro, plus find_ida_dir() for IDA installation discovery |
_cli.py |
Convenience CLI entry point — re-mcp-ida is equivalent to re-mcp --backend ida (the ida-mcp alias package also points here) |
| Module | Role |
|---|---|
backend.py |
GhidraBackend — implements the Backend protocol; registers open_database, Ghidra-specific LLM instructions, and target listing |
server.py |
Worker entry point (re-mcp-ghidra-worker) — creates GhidraServer (a BackendServer subclass), auto-discovers and registers all tool modules from tools/, runs stdio transport |
session.py |
Database session singleton (per worker), require_open decorator |
exceptions.py |
GhidraError(BackendError) — Ghidra-specific structured error type |
helpers.py |
Address parsing, formatting, pagination, resolution helpers, MCP annotation presets, Annotated parameter type aliases, call_ghidra main-thread dispatch |
models.py |
Re-exports shared Pydantic models from re_mcp.models for convenient single-source imports |
transforms.py |
Ghidra-specific tool visibility constants — PINNED_TOOLS and MANAGEMENT_TOOLS frozensets |
resources.py |
MCP resources — read-only, cacheable context endpoints |
prompts/ |
MCP prompt stub (no prompts registered yet) |
__init__.py |
Lazy bootstrap() to start pyghidra/JVM, plus find_ghidra_dir() for Ghidra installation discovery |
_cli.py |
Convenience CLI entry point — re-mcp-ghidra is equivalent to re-mcp --backend ghidra |
Each backend has a tools/ directory with identically-named tool modules. Both backends follow the same pattern:
from fastmcp import FastMCP
from <backend>.helpers import ANNO_READ_ONLY, Address, Limit, Offset
from <backend>.session import session
def register(mcp: FastMCP):
@mcp.tool(annotations=ANNO_READ_ONLY, tags={"domain"})
@session.require_open
def my_tool(address: Address, offset: Offset = 0, limit: Limit = 100) -> MyToolResult:
"""Tool description for LLM consumption.
Args:
address: Address of the thing.
"""
# Implementation using backend-specific APIs
return MyToolResult(result="...")Key conventions:
- Backend-specific imports are top-level (safe because the worker calls
bootstrap()before importing tool modules). Tool modules are auto-discovered viapkgutil.iter_modules— anytools/*.pywith aregister(mcp)function is loaded automatically @session.require_openis applied to all worker tools that need a database- Every tool has MCP annotations (
ANNO_READ_ONLY,ANNO_MUTATE,ANNO_MUTATE_NON_IDEMPOTENT, orANNO_DESTRUCTIVE) andtags=for categorical grouping. IDA tools may also havemeta=presets (META_DECOMPILER,META_BATCH,META_READS_FILES,META_WRITES_FILES) for static metadata - Use
Annotatedtype aliases (Address,Offset,Limit,FilterPattern,HexBytes) for parameter types — they embed descriptions and validation constraints directly into the JSON schema.OperandIndexis available in the IDA backend only - Tool docstrings are sent to the LLM as tool descriptions — they should be clear and concise
- Tools that accept addresses use
resolve_addressorresolve_functionfrom helpers
The tool modules are organized by domain. Both backends share the same module names and tool names. Some modules contain both read and write operations; the grouping reflects the primary purpose of each module.
Query-oriented tools (primarily read operations):
functions.py— function listing, info, disassembly, decompilation, renaming, deletion, boundsxrefs.py— cross-reference queries, call graphssearch.py— string extraction, byte/text/immediate search, string-to-code reference lookupdata.py— raw byte reading, segment listing, pointer table readingimports_exports.py— imports, exports, entry points, import name/ordinal modificationcfg.py— basic blocks, CFG edgesoperands.py— instruction decoding, operand value resolutionframes.py— stack frames, local variablesctree.py— AST explorationprocessor.py— architecture info, instruction classification, instruction set listingswitches.py— switch/jump table analysisregfinder.py— register value trackingnalt.py— address metadata: source line numbers, analysis flags, library item markinganalysis.py— auto-analysis control, analysis problems, fixups, exception handlers, segment registersexport.py— batch decompilation/disassembly export, output file generation
Mutation-oriented tools (primarily write operations):
database.py— database lifecycle, metadata, flags, file region mappingchunks.py— function chunk (tail) listing and managementpatching.py— byte patching, function/code creation, undefineassemble.py— instruction assemblycomments.py— comment managementnames.py— address renamingtypes.py,typeinf.py— type application and local type managementfunction_type.py— function prototypes and calling conventionsstructs.py— structure CRUDenums.py— enum CRUDdecompiler.py— pseudocode variable listing/renaming/retyping, microcode, decompiler commentsoperand_repr.py— operand display changessegments.py,rebase.py— segment manipulationxref_manip.py— cross-reference manipulationentry_manip.py— entry point addition, renaming, and forwardersmakedata.py— data type definitionload_data.py— loading bytes and additional binary files into databasefunc_flags.py— function flag and hidden range managementregvars.py— register variable add/delete/rename/commentsrclang.py— source declaration parsing via compiler parsers
Utility tools:
utility.py— number conversion, expression evaluation, script executionbookmarks.py— bookmark managementcolors.py— address/function coloringundo.py— undo/redosnapshots.py— database snapshot take/list/restoredirtree.py— directory tree managementsignatures.py,sig_gen.py— signature libraries and type librariesdemangle.py— C++ name demangling
MCP resources provide read-only context about the open database. They are defined in each backend's resources.py and reserved for genuinely static or aggregate data that benefits from caching:
- Static binary data — imports, exports, entry points (baked into the binary, stable), each with a regex search variant
- Aggregate snapshot — statistics (function/segment/entry point/string/name counts, code coverage)
The supervisor also owns one resource (<scheme>://databases) that lists all open databases with worker state.
Worker resources are exposed through RoutingTemplate instances in WorkerPoolProvider. Each RoutingTemplate stores the original worker URI template as _backend_uri_template and presents a prefixed version with {database} to clients (e.g. ida://idb/imports{?offset,limit} becomes ida://{database}/idb/imports{?offset,limit}). At read time, _read() pops database from the params to resolve the worker, then reconstructs the backend URI from the stored template with the remaining params. The supervisor's own database list resource is registered directly on the FastMCP server and served by FastMCP's internal local provider — the provider chain handles routing automatically.
MCP prompts provide guided analysis workflow templates. They are currently defined only in the IDA backend's prompts/ directory (the Ghidra backend has a stub prompts/ package but no prompts registered yet). Modules are organized by domain:
analysis.py— binary triage (survey_binary), function analysis (analyze_function), before/after diff (diff_before_after), function classification (classify_functions)security.py— cryptographic constant scanning (find_crypto_constants)workflow.py— string-based rename suggestions (auto_rename_strings), ABI type application (apply_abi), annotation export script generation (export_idc_script)
Prompts are registered on the supervisor by the backend (via register_prompts()). Workers do not register or handle prompts.
The process is the same for both backends. Using IDA as an example:
- Create
packages/re-mcp-ida/src/re_mcp_ida/tools/newtool.pywith aregister(mcp: FastMCP)function - Define tool functions inside
register()using@mcp.tool()and@session.require_open - Add
annotations=(ANNO_READ_ONLY,ANNO_MUTATE,ANNO_MUTATE_NON_IDEMPOTENT, orANNO_DESTRUCTIVE) andtags=to@mcp.tool() - Use
Annotatedtype aliases for parameters:Address,Offset,Limit,FilterPattern,HexBytes(from core helpers);OperandIndexis IDA-only - Tool modules are auto-discovered — any
tools/*.pywith aregister()function is loaded automatically - Use helpers from
helpers.py—resolve_address,resolve_function,paginate, etc. - Return Pydantic model instances on success; raise the backend's error type on failure (do not return error dicts)
- Add any new third-party imports to the
known-third-partylist inpyproject.tomlunder[tool.ruff.lint.isort] - Ideally add the tool to both backends with matching names and parameters for portability
For the Ghidra backend, the same steps apply under packages/re-mcp-ghidra/.
ida_ida.get_inf_structure()is removed — use free functions:ida_ida.inf_get_min_ea(),ida_ida.inf_get_max_ea(),ida_ida.inf_get_start_ea(),ida_ida.inf_get_app_bitness(),ida_ida.inf_is_64bit(), etc.- IDAPython
.somodules use stable ABI (no cpython version tag) — works with Python 3.12+ idapro.open_database(path, run_auto_analysis)returns 0 on success- The target binary must be in a writable directory (IDA creates
.i64alongside it) - idalib is single-threaded — all calls must be on the thread that imported
idapro