{
  "markdown": "# GhidraLens\n\n**Ghidra, rendered inside your AI client. Click a symbol to rename it. Click a call to follow it.**\n\n[![CI](https://github.com/hellosverre/ghidralens/actions/workflows/ci.yml/badge.svg)](https://github.com/hellosverre/ghidralens/actions/workflows/ci.yml)\n[![MCP Apps](https://img.shields.io/badge/MCP-Apps%20extension-0e7490)](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp)\n[![Ghidra 11.3+](https://img.shields.io/badge/Ghidra-11.3%2B-b45309)](https://ghidra-sre.org/)\n[![Local models](https://img.shields.io/badge/local%20models-Ollama-5fd3e8)](#running-it-on-a-local-model)\n[![npm](https://img.shields.io/npm/v/ghidralens?color=b45309&label=npm)](https://www.npmjs.com/package/ghidralens)\n[![MIT](https://img.shields.io/badge/licence-MIT-64748b)](LICENSE)\n\n![The decompiler view](docs/img/decompiler.png)\n\nEvery Ghidra MCP server so far returns text. The model can read it; you cannot\nnavigate it. GhidraLens returns the same analysis as an **interactive view** —\nbuilt on [MCP Apps](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp)\n(`io.modelcontextprotocol/ui`), the extension that lets a server ship real HTML\ninto the conversation.\n\nYou and the model are looking at the same live program. Rename a variable by\nclicking it and the model's next decompile sees the new name.\n\nThat is a real screenshot: `where.exe`, decompiled by Ghidra, every identifier\ncarrying the address it came from.\n\n---\n\n## What you get\n\n| View | What it does |\n| --- | --- |\n| **Decompiler** | Ghidra's C output as a live token stream — every identifier carries its address and its kind. Click a local to rename it, click a call to follow it. Callers, callees and variables in a sidebar. |\n| **Function browser** | Every function in the binary, filterable and sortable by address, name, size or caller count. Click a row to decompile it. |\n| **Call graph** | Callers to the left, callees to the right, the function you asked about in the middle. Click any node to recenter. |\n\n### Function browser\n\n![The function browser](docs/img/functions.png)\n\n### Call graph\n\n![The call graph](docs/img/callgraph.png)\n\nTen tools total. Three open views; the rest are lookups and writes, including\ntwo the model never sees — they exist only so a click in a view can fire them.\n\n## How it fits together\n\n```\n  MCP client  ──stdio──▶  server/  ──HTTP──▶  bridge/  ──JPype──▶  Ghidra (JVM)\n  (Claude,                 TypeScript         PyGhidra            program stays\n   Cursor, …)              MCP server         session             resident\n       ▲\n       │  ui:// HTML in a sandboxed iframe\n       └──  ui/  three self-contained views\n```\n\nThe bridge is a separate long-lived process on purpose. Ghidra's auto-analysis\nis the expensive step, and it happens **once**. Measured on a 64 KB Windows\nsystem utility (198 functions):\n\n| | |\n| --- | --- |\n| First open, with analysis | **25 s** |\n| Re-open the same binary | **0.3 s** |\n| Decompile one 2 KB function | **0.4 s** |\n| 87-node call graph | **< 0.1 s** |\n\nRestart the MCP server or the client and the analysed program is still there.\n\n## Setup\n\n**Prerequisites:** Ghidra 11.3+, a JDK 21+, Python 3.9–3.13 (**not 3.14** —\nJPype has no wheel for it yet), Node 20+. See\n[bridge/setup.py.md](bridge/setup.py.md) — the Python side is fussy and that file\ncovers every way it goes wrong. GhidraLens finds a JDK for you if `JAVA_HOME` is\nunset, which covers the usual \"installed Java, shell has not restarted\" case.\n\n```bash\ngit clone https://github.com/hellosverre/ghidralens\ncd ghidralens\nnpm install\nnpm run build\n```\n\nThen start the bridge on the binary you want to look at:\n\n```bash\npython bridge/serve.py --binary /path/to/target.exe\n```\n\nIt prints a token. Put that, and the path to the built server, into your MCP\nclient config:\n\n```json\n{\n  \"mcpServers\": {\n    \"ghidralens\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ghidralens\"],\n      \"env\": {\n        \"GHIDRALENS_BRIDGE_URL\": \"http://127.0.0.1:8799\",\n        \"GHIDRALENS_TOKEN\": \"paste-the-printed-token-here\"\n      }\n    }\n  }\n}\n```\n\nRunning from a clone instead? Point `command` at `node` and `args` at\n`/absolute/path/to/ghidralens/server/dist/index.js`.\n\nThen ask your client: *\"decompile the function that handles license validation\"*.\n\nAlso listed in the official MCP Registry as `io.github.hellosverre/ghidralens`.\n\n> **Editing the config by hand?** Quit the client first — properly, including any\n> system-tray icon. Claude Desktop keeps its own copy of\n> `claude_desktop_config.json` in memory and writes it back over yours when it\n> exits, so an edit made while it is running silently disappears on the next\n> restart. Editing through Settings → Developer → Edit Config avoids the race\n> entirely.\n\n## Tools\n\n| Tool | Visible to | Renders |\n| --- | --- | --- |\n| `open_binary` | model | — |\n| `program_info` | model | — |\n| `decompile` | model + view | Decompiler |\n| `list_functions` | model + view | Function browser |\n| `call_graph` | model + view | Call graph |\n| `find_strings` | model | — |\n| `xrefs_to` | model + view | — |\n| `rename_symbol` | model + view | — |\n| `add_comment` | **view only** | — |\n| `save_program` | model | — |\n\n`add_comment` is hidden from the model deliberately. Visibility is how MCP Apps\nseparates \"the agent may do this\" from \"a click may do this\"; keeping write\ntools out of the model's list keeps it short and stops the model from renaming\nthings on its own initiative.\n\nRenames and comments live in memory until `save_program` writes them into the\nGhidra project — after which they show up in the Ghidra GUI like any other edit.\n\n## Developing the views without Ghidra\n\n```bash\nnpm run dev:ui\n# open http://localhost:5173/dev/harness.html\n```\n\n`ui/dev/harness.ts` is a **real MCP Apps host** — it runs the SDK's `AppBridge`\nagainst the view in an iframe, so the `ui/initialize` handshake, the opening\n`ui/notifications/tool-result`, and every `tools/call` a click fires all go over\nreal postMessage JSON-RPC. There is a message trace down the right-hand side and\na host-theme switch, because the views have to look right in both.\n\nTwo data sources, switchable in the toolbar:\n\n- **fixtures** — no Ghidra needed, nothing to install\n- **live bridge** — proxies to a running bridge, so you develop against a real\n  analysed program\n\nUse live before you trust anything. Fixtures are tidy; real output is a\n400-line function with 56 locals and an 87-node call graph, and that is where\nlayout actually breaks.\n\n## Running it on a local model\n\nReverse engineering is exactly the work people would rather not send to a hosted\nmodel, so `agent/ollama-agent.mjs` is a small MCP host that puts GhidraLens\nbehind [Ollama](https://ollama.com) instead. No API key, nothing leaves the\nmachine.\n\n```bash\nOLLAMA_MODEL=qwen3:14b node agent/ollama-agent.mjs \"what does this binary do?\"\n```\n\nIt respects `_meta.ui.visibility`, so the app-only tools stay hidden from the\nmodel — the same separation a graphical client enforces. A ~9B model is enough to\norient itself with `find_strings` and `list_functions`; a 14B is noticeably\nbetter at reading decompiled C.\n\n## Tests\n\n| Suite | Needs Ghidra | Covers |\n| --- | --- | --- |\n| `node server/smoke.mjs` | no | MCP surface: tools, `ui://` resources, tool visibility, graceful failure with no bridge |\n| `python bridge/test_serve.py` | no | Bridge auth, CSRF rejection, routing, input validation |\n| `python bridge/test_session.py` | **yes** | Every Ghidra call: analysis, caching, decompiler tokens, imports, renames, writes |\n| `node server/live.mjs` | **yes** (bridge running) | The whole chain, and that every payload matches the shape the views index into |\n\nThe first two are what CI can run. `test_session.py` is the one that matters\nafter touching `bridge/session.py` — it is the only thing that proves the Ghidra\nAPI calls are right, and it caught three real bugs the day it was written.\n\n## Containerising it\n\nThe `Dockerfile` builds the server and views for clients or registries that want\nto start it themselves. One thing to get right:\n\n```dockerfile\nCMD [\"node\", \"server/dist/index.js\"]     # correct\nCMD [\"npm\", \"run\", \"start\"]              # breaks the protocol\n```\n\nA stdio MCP server speaks JSON-RPC on stdout, and `npm run` / `pnpm run` print\nthe script banner there first:\n\n```\n> ghidralens@0.1.1 start\n> node server/dist/index.js\n```\n\nThose lines land in the stream ahead of the handshake and the client gives up\nmid-initialize. The symptom is unhelpful - the container builds, starts, exits\ncleanly, and the client just reports no tools - so it is worth not stepping on.\nInvoke node directly.\n\nReal analysis still needs the bridge on the host: `127.0.0.1` inside a container\nis the container, so point `GHIDRALENS_BRIDGE_URL` at `host.docker.internal` or\na real address.\n\n## Security\n\nThe bridge binds `127.0.0.1` only, requires a per-run token in\n`X-GhidraLens-Token`, and rejects any request carrying an `Origin` or `Referer`\nheader — so a page open in your browser cannot reach your decompiler. It has no\nmulti-user model and is not meant to be exposed; `--host` refuses anything but\nloopback.\n\nAnalysing a binary does not execute it, but Ghidra will happily open malware.\nUse the same isolation you would use for any other RE work.\n\n## Licence\n\nMIT.\n",
  "bytes": 9326,
  "sha": "dfc083903dcd50f1e815a45203a3d32968e0294b9cf13ca61fdcc88447f3f6e6",
  "repo_slug": "hellosverre/ghidralens",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_hellosverre_ghidralens_cea98855/readme"
}