{"id":"cbac1cc2-3d0a-4604-a74f-45d10e8ce959","slug":"ida-mcp-rs","kind":"mcp","name":"ida-mcp-rs","summary":"Headless IDA Pro MCP Server","owner":"blacktop","repoUrl":"https://github.com/blacktop/ida-mcp-rs","license":"MIT","licenseTier":"permissive","trust":"community","stars":868,"match":null,"fewStars":false,"format":"mcp","createdAt":"2026-01-01T22:29:48.000Z","addedAt":"2026-10-14T03:42:09.381Z","logoUrl":"https://avatars.githubusercontent.com/u/6118188?u=f727ce4b73c85ae2d6eb0eda1ff73c0027afdad4&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["browser-web","cloud-infra"],"tags":["headless","idalib","idapro","rust","rust-lang"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-15T02:31:39.359Z","sample":false,"url":"https://conduits.dev/tools/ida-mcp-rs","language":"Rust","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <!--<a href=\"https://github.com/blacktop/ida-mcp-rs\"><img alt=\"Logo\" src=\"https://raw.githubusercontent.com/blacktop/ida-mcp-rs/refs/heads/main/docs/logo.svg\" height=\"400\"/></a>-->\n  <h1 align=\"center\">ida-mcp-rs</h1>\n  <h4><p align=\"center\">Headless IDA Pro MCP server for AI-powered reverse engineering.</p></h4>\n  <p align=\"center\">\n    <a href=\"https://github.com/blacktop/ida-mcp-rs/actions\" alt=\"Actions\">\n          <img src=\"https://github.com/blacktop/ida-mcp-rs/actions/workflows/build.yml/badge.svg\" /></a>\n    <a href=\"https://github.com/blacktop/ida-mcp-rs/releases/latest\" alt=\"Downloads\">\n          <img src=\"https://img.shields.io/github/downloads/blacktop/ida-mcp-rs/total.svg\" /></a>\n    <a href=\"https://github.com/blacktop/ida-mcp-rs/releases\" alt=\"GitHub Release\">\n          <img src=\"https://img.shields.io/github/v/release/blacktop/ida-mcp-rs\" /></a>\n    <a href=\"http://doge.mit-license.org\" alt=\"LICENSE\">\n          <img src=\"https://img.shields.io/:license-mit-blue.svg\" /></a>\n</p>\n<br>\n\n## Why ida-mcp\n\n- **Built for Apple targets.** Universal Mach-O slice selection, dSYM/DWARF\n  loading, and dyld_shared_cache module and dylib loading are single tool\n  calls with explicit parameters, not scripts the agent has to write. In a\n  measured Claude Code run, selecting and analyzing the x86_64 slice of a\n  universal binary took one call and about five seconds.\n- **Fast and lean.** A native Rust server driving IDA through idalib; a 28-tool\n  `--profile=lean` measured 25% cheaper than the full set on real tasks with\n  the same correctness, and the full set is there when you need it.\n- **Safe to leave running.** Exact-target edits, `save_idb` checkpoints,\n  analysis flushed to disk as soon as it finishes, graceful shutdown on every\n  signal a client sends, and database state discarded rather than trusted\n  after an IDA crash.\n- **Scriptable when the tools run out.** `run_script` returns a trailing\n  expression as JSON and keeps state between calls, so one script can find,\n  filter, and return only what matters.\n\n## Prerequisites\n\n- IDA Pro 9.4 with a valid license\n- IDA Pro 9.5 beta for the [beta release](#ida-95-beta)\n\n## Getting started\n\n### Install\n\n**macOS / Linux** (via [Homebrew](https://brew.sh))\n```bash\nbrew install blacktop/tap/ida-mcp        # Latest (IDA 9.4)\n```\n\n**macOS (Apple Silicon), older IDA releases** (via versioned Homebrew casks)\n```bash\nbrew install blacktop/tap/ida-mcp@9.3    # IDA 9.3/9.3sp1\nbrew install blacktop/tap/ida-mcp@9.2    # IDA 9.2\n```\n\n**Windows** (via [Scoop](https://scoop.sh))\n```powershell\nscoop bucket add blacktop https://github.com/blacktop/scoop-bucket\nscoop install blacktop/ida-mcp\n```\n\n> **Windows note:** see [Windows](#windows) below for DLL discovery options.\n\n**macOS / Linux** (via [Nix](https://nixos.org))\n```bash\nnix shell github:blacktop/nur#ida-mcp \\\n  --extra-experimental-features 'nix-command flakes'\n```\n\n**Direct download:** grab the archive for your platform from [GitHub Releases](https://github.com/blacktop/ida-mcp-rs/releases).\n\n**Build from source:** see [docs/BUILDING.md](docs/BUILDING.md).\n\n> ida-mcp versions follow IDA Pro versions: `v9.5.0-beta.x` for the IDA 9.5 beta, `v9.4.x` for IDA 9.4, `v9.3.x` for IDA 9.3, and `v9.2.x` for IDA 9.2. ida-mcp checks compatibility when it initializes IDA. An incompatible version causes IDA-backed tools to fail with an error identifying the detected version and, when available, the loaded library path. Scoop and NUR publish only the latest version. For an older IDA, use the matching [GitHub Release](https://github.com/blacktop/ida-mcp-rs/releases) or, on Apple Silicon, a versioned Homebrew cask.\n\n#### IDA 9.5 beta\n\nA prerelease build for the IDA 9.5 beta is available for macOS on Apple Silicon:\n```bash\nbrew install blacktop/tap/ida-mcp@beta   # IDA 9.5 beta\n```\n\nThe beta cask conflicts with the stable `ida-mcp` cask, so remove one before installing the other:\n```bash\nbrew uninstall --cask ida-mcp            # switch to the beta\nbrew uninstall --cask ida-mcp@beta       # switch back to stable\n```\n\nYou can also download `ida-mcp_<version>_Darwin_arm64.tar.gz` from the [prerelease on GitHub](https://github.com/blacktop/ida-mcp-rs/releases). There are no beta packages for Linux or Windows; build `main` from source as described in [docs/BUILDING.md](docs/BUILDING.md).\n\n### Platform setup\n\n#### macOS\n\nStandard IDA installs in `/Applications` work without extra setup:\n```bash\nclaude mcp add ida -- ida-mcp\n```\n\nIf you see `Library not loaded: @rpath/libida.dylib`, point `DYLD_LIBRARY_PATH` at your IDA install:\n```bash\nclaude mcp add ida -e DYLD_LIBRARY_PATH='/path/to/IDA.app/Contents/MacOS' -- ida-mcp\n```\n\nPaths found automatically:\n- `/Applications/IDA Professional 9.4.app/Contents/MacOS`\n- `/Applications/IDA Pro 9.4.app/Contents/MacOS`\n- `/Applications/IDA Home 9.4.app/Contents/MacOS`\n- `/Applications/IDA Essential 9.4.app/Contents/MacOS`\n\nThe beta searches the same locations with `9.5` in place of `9.4`.\n\n#### Linux\n\nThe IDA installer defaults to `~/ida-pro-9.4`, and the launcher script looks there:\n```bash\nclaude mcp add ida -- ida-mcp\n```\n\nFor any other install location, set `IDADIR`:\n```bash\nclaude mcp add ida -e IDADIR='/path/to/ida' -- ida-mcp\n```\n\nLookup order: `$IDADIR`, then `~/ida-pro-9.4`, then `/opt/ida-pro-9.4` and the other RUNPATH fallbacks.\n\n#### Windows\n\n**Option A:** put `ida-mcp.exe` in your IDA directory. This is the simplest route and needs no environment setup:\n```powershell\n# Copy the binary next to ida.dll / idalib.dll\ncopy ida-mcp.exe \"C:\\Program Files\\IDA Professional 9.4\\\"\nclaude mcp add ida -- \"C:\\Program Files\\IDA Professional 9.4\\ida-mcp.exe\"\n```\n\n**Option B:** install with [Scoop](https://scoop.sh), which finds IDA and sets `IDADIR`:\n```powershell\nscoop bucket add blacktop https://github.com/blacktop/scoop-bucket\nscoop install blacktop/ida-mcp\nclaude mcp add ida -- ida-mcp\n```\n\n**Option C:** set `IDADIR` yourself:\n```powershell\n$idaDir = \"C:\\Program Files\\IDA Professional 9.4\"\n[Environment]::SetEnvironmentVariable(\"IDADIR\", $idaDir, \"User\")\n$userPath = [Environment]::GetEnvironmentVariable(\"Path\", \"User\")\n$pathEntries = @($userPath -split \";\" | Where-Object { $_ })\nif (-not ($pathEntries -contains $idaDir)) {\n  [Environment]::SetEnvironmentVariable(\n    \"Path\", (@($pathEntries + $idaDir) -join \";\"), \"User\"\n  )\n}\n# Then restart your terminal\nclaude mcp add ida -- ida-mcp\n```\n\nWindows has to find `ida.dll` and `idalib.dll` before `ida-mcp` starts. Putting `ida-mcp.exe` in the IDA directory is the easiest way. Otherwise, set `IDADIR` and add the same directory to `PATH` so the DLLs load at runtime.\n\nCommon IDA paths:\n- `C:\\Program Files\\IDA Professional 9.4`\n- `C:\\Program Files\\IDA Pro 9.4`\n- `C:\\Program Files\\IDA Home 9.4`\n\n### Runtime requirements\n\nThe binary links against IDA's libraries at runtime. Baked-in RPATHs cover the standard install paths. For anything else:\n\n| Platform | Library | Fallback Configuration |\n|----------|---------|------------------------|\n| macOS | `libida.dylib` | `DYLD_LIBRARY_PATH` |\n| Linux | `libida.so` | `IDADIR` (launcher reads it) or `LD_LIBRARY_PATH` |\n| Windows | `ida.dll` | Place exe in IDA dir, or set `IDADIR` and add IDA dir to `PATH` |\n\n### Configure your AI agent\n\n#### [Claude Code](https://code.claude.com/docs/en/mcp)\n```bash\nclaude mcp add ida -- ida-mcp\n```\n\n#### [Codex CLI](https://github.com/openai/codex)\n```bash\ncodex mcp add ida -- ida-mcp\n```\n\n#### [Gemini CLI](https://github.com/google-gemini/gemini-cli)\n```bash\ngemini mcp add ida -- ida-mcp\n```\n\n#### [Cursor](https://cursor.com)\nAdd to `.cursor/mcp.json`:\n```json\n{\n  \"mcpServers\": {\n    \"ida\": { \"command\": \"ida-mcp\" }\n  }\n}\n```\n\n### Usage\n\nOnce your agent is configured, it can drive the tools directly:\n\n```\n# Open a binary. This returns quickly; analysis runs separately.\nopen_idb(path: \"~/samples/malware\")\n\n# Keep the generated database out of a read-only input directory\nopen_idb(path: \"/System/example\", idb_out: \"~/ida-work/example.i64\")\n\n# Pick one slice of a universal (fat) Mach-O\nopen_idb(path: \"/bin/ls\", arch: \"arm64e\", idb_out: \"~/ida-work/ls.i64\")\n\n# These work immediately, no analysis needed\nlist_functions(limit: 20)\ndisasm_by_name(name: \"main\", count: 20)\nstrings(limit: 10)\n\n# For xrefs/decompile on large binaries, run analysis in the background\nanalyze_funcs(background: true)   # returns task_id\ntask_status(task_id: \"analyze-<random>\") # poll progress (ID from the analyze_funcs response)\n\n# Decompile (requires Hex-Rays + completed analysis)\ndecompile(address: \"0x100000f00\")\n\n# Discover more tools\ntool_catalog(query: \"find callers\")\n```\n\nTools that change the database (`rename`, `set_comments`, `patch`, `patch_asm`,\n`apply_types`, `declare_stack`, `delete_stack`, and `lumina_apply`) take exactly\none address or one symbol name. Names must match exactly, case included. A near\nmiss changes nothing and returns up to eight similar names with their addresses.\nA returned `target` record identifies the database, resolved symbol (or `null`),\nand requested and effective addresses. Check the operation's result to determine\nwhether the change succeeded.\n\nFor Hex-Rays locals and arguments, use `list_lvars` with one function address\nor exact `target_name`. `rename_lvar` and `set_lvar_type` take the same function\nselector plus exactly one of `lvar_locator` or `lvar_name`. Prefer the opaque\n`locator` returned by `list_lvars`: display names such as `v1` can move to a\ndifferent local after a prototype change. A stale or ambiguous locator fails\nwithout making a change. Locals without a locator are edited by exact, unique\nname.\nThe tools return the variable as it was before the edit and the resolved\nfunction target. Use `save_idb` to checkpoint edits to disk.\n\n```text\nlist_lvars(target_name: \"interesting_function\")\nrename_lvar(target_name: \"interesting_function\", lvar_locator: \"<locator from list_lvars>\", new_name: \"count\")\nset_lvar_type(target_name: \"interesting_function\", lvar_locator: \"<locator from list_lvars>\", decl: \"unsigned int\")\nsave_idb()\n```\n\n`list_lvars` is in the `decompile` toolset, `rename_lvar` in `editing`, and\n`set_lvar_type` in `types`. Add them to lean with\n`--profile=lean --tools=list_lvars,rename_lvar,set_lvar_type`.\n\nFor end-of-line comments in Hex-Rays pseudocode, use\n`list_pseudocode_comments` with one function address or exact `target_name`.\nIt returns each commentable line's opaque `locator`, address, text, and\nexisting comment, paginated like `list_lvars`. Pass one `locator` to\n`set_pseudocode_comment` as `comment_locator`; an empty comment removes it. A stale locator fails\nwithout making a change; list the locations again after reanalysis.\n`set_comments` edits disassembly comments, which pseudocode does not show.\n\n```text\nlist_pseudocode_comments(target_name: \"interesting_function\")\nset_pseudocode_comment(target_name: \"interesting_function\", comment_locator: \"<locator from list_pseudocode_comments>\", comment: \"validated input\")\nsave_idb()\n```\n\n`list_pseudocode_comments` is in the `decompile` toolset and\n`set_pseudocode_comment` in `editing`.\n\n## Opening binaries\n\n### Raw blobs\n\nRaw inputs use IDA's normal loader by default and save to `<input>.i64`. Pass\n`idb_out` to put the database somewhere else, for example when the input\ndirectory is read-only. An existing database is reused only when the input\nSHA-256 recorded in it matches the current file. `rebuild: true` overwrites a\ndatabase only when its recorded hash or input path shows it was built from this\ninput.\n\nFor headerless blobs, `open_idb` also takes typed loader hints:\n\n```text\nopen_idb(\n  path: \"~/firmware/boot.bin\",\n  idb_out: \"~/ida-work/boot.i64\",\n  processor: \"arm:ARMv7-M\",\n  bitness: 32,\n  base_address: \"0x08000000\",\n  entry_point: \"0x08000101\"\n)\n```\n\nProcessor families with several modes need an explicit IDA processor variant,\nso bare names such as `arm` or `metapc` are rejected. The hints apply only when\ncreating a database from a raw input; they never change an existing\n`.i64`/`.idb`. On 32-bit ARM, an odd `entry_point` is a Thumb pointer: ida-mcp\nclears bit 0 for the code address and records the Thumb state before creating\nthe entry instruction.\n\n### Universal (fat) Mach-O\n\nWhen IDA loads a fat binary headlessly, it takes the x86_64 slice whenever one\nexists, and it doesn't recognize 64-bit fat headers at all. So `open_idb` picks\nthe slice itself. Pass `arch` (for example `arm64e` or `x86_64`), or leave it out\nand answer the prompt if your client supports input requests.\n\nIf no answer comes back, the call fails and lists the slices so an unattended\nagent can retry with `arch`. That covers clients that can't prompt, a declined\nprompt, and pre-2026 clients that don't reply within 30 seconds. MCP 2026\nclients get the question back in the response and retry with the answer, so\nthe server never sits waiting on them.\n\nThe chosen slice is copied to `<input name>.<arch>` next to the output database\nand opened as a single-architecture Mach-O. An identical copy already at that\npath is reused; a different file there is never overwritten. The response's\n`universal` field names the slice, and `loader` shows what IDA loaded.\n\n### `dyld_shared_cache`\n\n`open_dsc` opens one module from Apple's dyld_shared_cache. On IDA 9.4, ida-mcp\nreads the DSC header directly and loads images through IDA's native `dscu`\nservice. Older IDA builds fall back to the legacy `idat` background flow when a\n`.i64` has to be generated.\n\n```\n# Open a module from the DSC\nopen_dsc(path: \"/path/to/dyld_shared_cache_arm64e\", arch: \"arm64e\",\n         module: \"/usr/lib/libobjc.A.dylib\")\n\n# If a legacy background task was started, poll until done\ntask_status(task_id: \"dsc-<random>\")  # ID from the open_dsc response\n\n# Load additional frameworks for cross-module references\nopen_dsc(path: \"/path/to/dyld_shared_cache_arm64e\", arch: \"arm64e\",\n         module: \"/usr/lib/libobjc.A.dylib\",\n         frameworks: [\"/System/Library/Frameworks/Foundation.framework/Foundation\"])\n\n# Incrementally load another DSC dylib into an already-open database\ndsc_add_dylib(module: \"/usr/lib/libSystem.B.dylib\")\n\n# Incrementally load a DSC data/GOT/stub region by address\ndsc_add_region(address: \"0x180116000\")\n\n# After dsc_add_dylib/dsc_add_region, confirm analysis readiness\nanalysis_status()\n```\n\nRequirements:\n- IDA 9.4+ for native `dscu` loading\n- For older IDA builds, `idat` must be available via `$IDADIR` or standard install paths\n\n## IDAPython scripting\n\n`run_script` runs Python in the open database through IDA's IDAPython engine\nand returns what the script wrote to stdout and stderr. A trailing expression\nis returned as `result`, so a script can compute, filter, and hand back only\nwhat matters:\n\n- `result` is strict JSON of at most 1 MiB. A larger result fails the call\n  rather than being truncated; return a smaller value or print a summary.\n- A value that is not JSON-serializable (custom objects, `NaN`/`Infinity`,\n  circular references) is returned as the string of its `repr()` with\n  `result_is_repr: true`. So is any value containing an integer outside\n  −2⁶³ … 2⁶⁴−1, which the server's JSON reader cannot hold exactly.\n\nImports, variables, and functions persist between calls on the same open\ndatabase.\n\n```\n# Inline script\nrun_script(code: \"import idautils\\nfor f in idautils.Functions():\\n    print(hex(f))\")\n\n# Run a .py file from disk\nrun_script(file: \"/path/to/analysis_script.py\")\n\n# Build state in one call, return a value from the next\nrun_script(code: \"import idautils\\nfuncs = list(idautils.Functions())\")\nrun_script(code: \"{'count': len(funcs), 'first': hex(funcs[0])}\")\n\n# With timeout (default 120s, max 600s)\nrun_script(code: \"import ida_bytes; print(ida_bytes.get_bytes(0x1000, 16).hex())\",\n           timeout_secs: 30)\n```\n\nAll `ida_*` modules, `idc`, and `idautils` are available. See the [IDAPython API reference](https://python.docs.hex-rays.com).\n\n## Structured results\n\n`analysis_status`, `list_functions`, `resolve_function`, `function_at`,\n`xrefs_to`, `xrefs_from`, `strings`, `list_lvars`, and\n`list_pseudocode_comments` advertise an `outputSchema` and return\n`structuredContent` alongside the same JSON as text. `rename_lvar`,\n`set_lvar_type`, and `set_pseudocode_comment` also return structured results\nbut omit their output schemas to keep the tool inventory smaller. Other tools\nreturn JSON text only; tools whose result is a bare array or a per-call shape\nhave no schema yet.\n\n## Saving and crash handling\n\n`save_idb` writes the open database to disk without closing it and returns\nthe database path it wrote. Use it to checkpoint renames, comments, types, and\npatches; `close_idb` also saves. If the client exits without closing, the\nserver attempts a graceful shutdown on stdin EOF and on SIGTERM, SIGINT,\nSIGQUIT, or SIGHUP, which closes and packs the open database; the log names\nthe signal. Default stdio and HTTP allow up to 120 seconds for the active\noperation to settle and the database to save before retiring the child. A SIGKILL\nskips graceful shutdown, and a caught\nSDK crash deliberately discards unsaved changes, so `save_idb` remains the\nonly guarantee for edits. Finished auto-analysis is flushed to the database\nas soon as a raw open or `analyze_funcs` completes, because some MCP clients\n(Claude Code among them) end servers with SIGKILL when the conversation ends;\nafter such a kill the analysis is on disk and only later edits are lost.\n\nIf a call crashes inside the IDA SDK (SIGSEGV/SIGBUS), ida-mcp returns an error\nfor that call (a top-level error even from batch tools that normally report\nper-item failures) and then stops using that database state: the database is closed\nwithout saving, so changes since the last `save_idb` are lost, and the child\nworker is replaced (stdio, HTTP, and `--workspace`). Call\n`open_idb` again to continue.\n\nThe default stdio and HTTP servers run IDA in one supervised child process (the same\nbinary in `worker` mode), as pooled HTTP (`--max-workers N`) and `--workspace`\ndo. A call that overruns its `timeout_secs` (or the 1800 s operation watchdog,\n`--workspace-worker-op-timeout-secs` for stdio, `--worker-op-timeout-secs` for\nHTTP) kills that child and returns a timeout:\nthe database is then no longer open, changes since the last `save_idb` are\nlost, and the next `open_idb` gets a fresh worker. If its replacement is still\nstarting, that open waits for startup, bounded to 30 seconds. This is what makes a\nnative IDA call that never returns recoverable; nothing can interrupt it in\nplace. On shared HTTP, cancelling a request or disconnecting stops that\nclient's wait; an already dispatched call continues under its watchdog so\nother clients keep their database and unsaved edits.\n\n### Removed tools\n\n| Removed | Use instead |\n|---|---|\n| `list_funcs` | `list_functions` (same parameters) |\n| `get_u8`, `get_u16`, `get_u32`, `get_u64` | `read_int` with `size` 1, 2, 4, or 8 |\n| `int_convert` | no replacement; do the conversion client-side or in `run_script` |\n\nUpdate `--tools` / `--exclude-tools` lists that name the removed tools; an\nunknown name is rejected at startup.\n\n## Multiple databases and HTTP\n\n### Workspace mode (opt-in)\n\nBy default ida-mcp works on one implicit database, so tool calls don't carry a\nhandle. To keep several databases open at once, start with `--workspace`:\n\n```bash\nida-mcp --workspace --workspace-max-workers 4\nida-mcp --workspace serve-http --bind 127.0.0.1:8765 --stateless\n```\n\nIn workspace mode, each `open_idb`/`open_dsc` returns a `database_id`, and every\ndatabase-scoped call must send it. Runtime tools such as `tool_catalog` reject\nit. `close_idb(database_id: ...)` closes only that handle. Idle handles are\nreaped after 30 minutes; `--workspace-idle-timeout-secs 0` turns that off.\n\n`list_databases` returns every routed `database_id` with its database path and\nstate (`open`, `busy`, or `no_worker`). An agent that lost a response, or\nreconnected over stateless HTTP, can use it to find an open database again\ninstead of leaving it stranded until the idle timeout. It is read-only and only\nappears when the server runs with `--workspace`.\n\n### HTTP/SSE worker pool\n\n`serve-http` shares one supervised IDA child by default. To serve several\nstateful HTTP/SSE clients at once, set `--max-workers` above `1`. Each session\nthen gets its own child `ida-mcp worker` process:\n\n```bash\nida-mcp serve-http --bind 127.0.0.1:8765 --max-workers 4 --min-workers 1\n```\n\nWithout `--max-workers N`, all HTTP sessions share one IDA context. A second\nclient that opens another binary waits behind the first, then gets the usual\n`A database is already open` error. Pooled startup logs include\n`Starting pooled HTTP router` and `MCP pooled HTTP server listening`.\n\nAn HTTP session holds its worker from the first open until `close_idb`, HTTP\n`DELETE`, session timeout, or server shutdown. `close_idb` releases the worker\nright away, but the child process may stay alive for reuse until\n`--worker-idle-timeout-secs` passes. When every worker is taken, new\n`open_idb`/`open_dsc` calls fail with `Worker pool exhausted` so clients can\nretry later. Pooled mode needs stateful HTTP sessions, so `--max-workers > 1`\nis rejected together with `--stateless`.\n\nIf an SSE client exits without sending `close_idb` or HTTP `DELETE`, pooled\nmode closes its session once the standalone SSE stream disconnects and the\n`--worker-disconnect-grace-secs` reconnect window runs out. POST-only clients\ndon't always leave a stream to watch, so their abandoned sessions are reclaimed\nby `--session-keep-alive-secs` (default 1800 seconds). Lower it if you need\npooled workers back sooner.\n\n### MCP 2026-07-28\n\nMCP `2026-07-28` uses the sessionless `server/discover` lifecycle. ida-mcp\nsupports it over stdio and in the default single-worker HTTP mode, including\nmulti-round elicitation and the `io.modelcontextprotocol/tasks` extension for\nbackground `open_dsc` calls.\n\nPooled HTTP (`--max-workers > 1`) advertises protocol versions only up to\n`2025-11-25`. Its workers are tied to sessions, and MCP 2026 has no session ID\nto route on, so a request could reach a different IDA worker. MCP 2026 requests\nto pooled HTTP fail with an unsupported protocol-version error instead.\n\n## Headless debugger (experimental)\n\nDebugger tools are off by default. On supported platforms (Apple Silicon macOS\nfor now), start with `--enable-debugger` or `IDA_MCP_ENABLE_DEBUGGER=true` to add\n`debug_status`, `debug_launch`, `debug_attach`, `debug_modules`, and\n`debug_stop`. `debug_open_module` also needs `--workspace`, because it opens the\nruntime image in a new database and leaves the database that owns the live\ndebug session alone:\n\n```text\ndebug_status()\ndebug_launch(database_id: \"…\", path: \"/absolute/path/to/program\")\ndebug_modules(database_id: \"…\")\ndebug_open_module(\n  database_id: \"…\",\n  module: \"/usr/lib/libobjc.A.dylib\",\n  idb_out: \"~/ida-work/libobjc.i64\"\n)\ndebug_stop(database_id: \"…\", action: \"auto\")\n```\n\n`debug_open_module` opens standalone binaries and dlopen'd plugins directly. On\nmacOS it also resolves system libraries such as `/usr/lib/libobjc.A.dylib`\nthrough the host dyld shared cache for the target's architecture, then loads the\nimage with IDA 9.4's in-process DSC service. It doesn't extract a temporary\ndylib or run `idat`.\n\n`debug_open_module` always requires `idb_out`. Runtime modules often live in\nread-only system directories, and ida-mcp won't silently write an IDB next to\nthem. The response includes a checked runtime slide; the new database stays at\nits on-disk preferred addresses.\n\nIn workspace mode, a launched or attached debug session pins its database so\nthe idle timeout skips it, until `debug_stop` succeeds or `close_idb` releases\nthe database.\n\nKnown limitations:\n\n- **A target that exits on its own keeps its pin.** IDA caches the process\n  state and refreshes it only when a call drains the pending debug event, so\n  ida-mcp can't see a debuggee die in the background. The database stays\n  pinned, and exempt from the idle timeout, until `debug_stop`, `close_idb`, or\n  worker loss clears it. Both `debug_stop` and `close_idb` handle an\n  already-exited target correctly.\n- **Losing the worker doesn't stop the debuggee.** If the worker hosting a live\n  session is killed, crashes, or is retired by ida-mcp after a wedged debugger\n  call, it may never run its own teardown. IDA's debug-server helper can then be\n  reparented instead of terminated, and the target may keep running. When an\n  in-flight call detects or causes the retirement, ida-mcp returns a\n  `Debugger session lost` error that says the target may still be alive, and\n  clears the handle's debug pin. It never claims the debuggee ended. A leased\n  worker can also die with no request in flight to carry an error;\n  `list_databases` may briefly show `no_worker` before the reaper removes that\n  handle, even with idle eviction disabled. Cleaning up a stray helper or\n  debuggee is manual for now.\n\nida-mcp picks IDA's signed loopback helper from the opened database's target\narchitecture: `mac_server_arm` for ARM64 and `mac_server` for x86/x86_64. macOS\nmay require IDA's \"Take Control\" authorization once per login; until it's\ngranted, tools report `user_action_required`. ida-mcp does not ask for root,\ndisable SIP, edit `authorizationdb`, or re-sign binaries.\n\nLinux and Windows don't advertise the debugger tools at all. IDA's ARM Linux\ndebugger only works remotely, and ida-mcp doesn't expose remote configuration.\nThe SDK has no Windows-on-ARM user-mode debugger. The native ARM64 test\nharnesses on both platforms check that the tools stay unavailable. x86 Linux\nand Windows stay off until a local debugger run passes on them.\n\n## Lumina\n\nida-mcp turns off IDA's automatic Lumina lookup by default, so starting the\nserver or opening a database doesn't contact `lumina.hex-rays.com`. The setting\nlives only in ida-mcp's private IDA user profile; your normal IDA GUI profile is\nuntouched. IDA on Windows keeps its settings in the registry rather than under\n`IDAUSR`, so there ida-mcp also uses a process-local registry mapping. If\nida-mcp can't set up the private profile, it refuses to start.\n\nTo let IDA use its configured Lumina servers, opt in for that process:\n\n```bash\nida-mcp --allow-lumina\n```\n\nThe environment variable equivalent is `IDA_MCP_ALLOW_LUMINA=true`.\n\nTwo tools use it. Both are listed by default, but until you opt in they fail with\nan error telling you to restart with `--allow-lumina`:\n\n- `lumina_lookup` queries one function and reports the available metadata\n  without changing the database.\n- `lumina_apply` pulls and applies metadata using IDA's upgrade policy.\n  `force: true` may replace existing names, types, or comments.\n\n`--read-only` removes `lumina_apply` but keeps `lumina_lookup`, since a lookup\ndoesn't modify the database.\n\n## Context optimization\n\nBy default `tools/list` returns 76 tools. The full tool list is roughly 15k\ntokens, estimated at four characters per token. Seven more are opt-in: the six\ndebugger tools and `list_databases` appear only with\n`--enable-debugger` or `--workspace`. Clients with dynamic tool discovery defer\nthe schema cost; clients that preload schemas pay it every session. To trim the\nsurface to what you need:\n\n| Flag | Env var | Effect |\n|---|---|---|\n| `--profile=lean`       | `IDA_MCP_PROFILE`       | Starts from the 28-tool `lean` set (below) instead of all tools |\n| `--toolsets=cat1,cat2` | `IDA_MCP_TOOLSETS` | Replaces \"all tools\" with the union of selected categories |\n| `--tools=t1,t2`        | `IDA_MCP_TOOLS`         | Adds individual tools (additive to `--toolsets`) |\n| `--exclude-tools=t1,t2`| `IDA_MCP_EXCLUDE_TOOLS` | Subtracts from the include set; always wins |\n| `--read-only`          | `IDA_MCP_READ_ONLY`     | Strips mutating/arbitrary-code tools (`run_script`, `save_idb`, `patch*`, `rename`, comment setters, `lumina_apply`, type/stack edits, `dsc_add_*`, `analyze_funcs`, and debugger process control); keeps lifecycle/discovery |\n\nWith no flags you get all 76 baseline tools. Categories: `core`, `functions`,\n`disassembly`, `decompile`, `xrefs`, `control_flow`, `memory`, `search`,\n`metadata`, `types`, `editing`, `scripting`; `debug` exists only when the\ndebugger is enabled on a supported platform (run `tool_catalog` to list them).\nFlags override env vars, and unknown names are rejected at startup.\n\n### The `lean` profile\n\n`--profile=lean` advertises 28 tools, about 26 KB of schemas, chosen for Apple\nreverse engineering: opening binaries, caches and debug info\n(`open_idb`, `open_dsc`, `dsc_add_dylib`, `dsc_add_region`, `load_debug_info`,\n`analysis_status`, `analyze_funcs`, `save_idb`, `close_idb`, `task_status`),\nreading code and references (`list_functions`, `resolve_function`,\n`function_at`, `disasm`, `decompile`, `xrefs_to`, `xrefs_from`, `callers`,\n`callees`, `strings`, `find_bytes`, `search`, `segments`, `imports`,\n`exports`), annotating (`rename`, `set_comments`), and `run_script` for\neverything else. `tool_catalog`/`tool_help` are left out because the schemas\nare already advertised; the server instructions adapt to whatever is enabled.\nThe profile composes with the other flags: `--tools` adds to it,\n`--exclude-tools` and `--read-only` subtract (`--profile=lean --read-only` is\n21 tools, 21.9 KB). The default stays the full set for compatibility; the\nprofile is the measured recommendation for Claude Code (below).\n\n```bash\nida-mcp --profile=lean\n```\n\nMeasured with `bench/` (Claude Code 2.1.288, Sonnet 5.5, medium effort,\nthree trials per cell, 2026-10-03) on an annotate / string-xrefs / universal\nMach-O task mix: both profiles completed all nine conversations correctly.\nLean cost 25% less than full ($0.275 vs $0.367), used 42 vs 52 tool calls,\nand recorded 0 vs 1 MCP tool errors; cache-read input fell from 849,325 to\n442,166 tokens. The saving is almost all cache-read tokens from the smaller\ninventory, so it applies to clients that send the tool list with every\nrequest. No other client was measured.\n\n### Recommendations by client\n\n- **Claude Code:** use `--profile=lean` for Apple RE work; it was 25% cheaper than the full set on the measured tasks with the same correctness. Add `--tools` for anything specific you need beyond it.\n- **Cursor:** defers MCP tool schemas and loads them on demand, so filtering is mainly about limiting what the agent can do; not measured.\n- **Codex CLI:** current models with tool search defer MCP tools automatically. For models without tool search, or to limit what the agent can do, pick a focused subset:\n  ```bash\n  ida-mcp --toolsets=core,functions,disassembly,decompile,xrefs\n  ```\n- **Clients without lazy tool loading:** every session receives the full tool list, estimated at ~15k tokens. Pick a focused subset as shown above.\n- **Gemini CLI:** filtering is optional, but a smaller surface cuts down on wrong tool picks when several MCP servers are enabled:\n  ```bash\n  ida-mcp --toolsets=core,functions,disassembly,decompile --read-only\n  ```\n- **Small / local models:** use the smallest surface that works. For triage:\n  ```bash\n  ida-mcp --toolsets=core,functions --tools=decompile,callees,callers --read-only\n  ```\n\n### Configuring through `mcpServers.json`\n\nMost MCP configs run `ida-mcp` with no subcommand. The env vars work there too:\n\n```json\n{\n  \"mcpServers\": {\n    \"ida-mcp\": {\n      \"command\": \"ida-mcp\",\n      \"env\": {\n        \"IDA_MCP_TOOLSETS\": \"core,functions,disassembly,decompile,xrefs\",\n        \"IDA_MCP_READ_ONLY\": \"true\"\n      }\n    }\n  }\n}\n```\n\n### Measuring\n\n`just measure-tools` prints a per-tool char/token breakdown. It starts the server without filter flags, so the numbers cover the full default list. To see what a filter saves, check your client's context view (`/context` in Claude Code, or its equivalent elsewhere).\n\n## Docs\n\n- [docs/TOOLS.md](docs/TOOLS.md) - Tool catalog and discovery workflow\n- [docs/TRANSPORTS.md](docs/TRANSPORTS.md) - Stdio vs Streamable HTTP\n- [docs/BUILDING.md](docs/BUILDING.md) - Build from source\n- [docs/TESTING.md](docs/TESTING.md) - Running tests\n\n## License\n\nMIT Copyright (c) 2026 **blacktop**\n"}