Skip to content
Hydra Engine

Write a client or MCP server

A checklist for tools that drive the daemon. One connection, a label per agent, adopting projects by hash, handling refusals, the wire, and pinning a release.

On this page

This page is for whoever writes the thing agents talk to: an MCP server, a harness plugin, a pipeline. The daemon is a generic Ghidra front end; the tools you expose, and what they check, belong to your client.

One connection per process

import ghidra_session as gs
 
s = gs.connect(client="my-mcp", harness="claude-code")   # joins the user's daemon, or starts it

The connection is thread-safe. Without parallel, one connection's requests run in the order sent, so a server that handles several tool calls at once sends parallel=True on each independent one. Name yourself: status, the log and every holder field use these names.

One label per agent

If one connection serves several agents, give each its own label, so a busy answer says which agent holds the lock and cancel stops only that agent's work: on main · 12.3-hydra.2

agent = s.labelled(client="agent-7", harness="claude-code")   # call, send, cancel, cancel_all

Adopt a project, do not import

Agents usually arrive with a binary that someone already analysed. Open the existing project read-only (no copy, no lock, nothing written), find the program by hash, and check the hash before trusting answers:

s.call("open_project", path=r"C:\re\Demo.gpr")
hit = s.call("list", sha256=digest)["entries"][0]
info = s.call("open_program", path=hit["key"])
assert info["sha256"] == digest

Only import when the binary is new, into a writable working project, with "analyze": true. The same file is never imported twice. For changes the master project must not see, fork it (Several writers).

Handle refusals by code

Every refusal is {"ok": false, "error": {"code", "message", "details"}}. The details of busy, conflict, not_owner and read_only name the holder and carry a suggest:

CodeIt meansDo this
busya slot or lock did not come free within wait (10 s)retry, or send a longer wait
not_owneranother client owns the program writablesend overlaySuggestion (private annotations), suggestion (a fork), or join with share when suggest says so
conflict, reason: "changed"the value changed since you read it (expect)read it again and decide
conflict, other reasonsthe project lock is another process's, the target exists, a copy is too largechange the request: retrying will not help
unsaved_changesclosing would drop changessave, or close with "discard": true

All codes are in the reference.

Long work

send returns a request id at once; wait collects the answer. Long commands emit progress events in between, and cancel takes the id:

rid = s.send("analyze", parallel=True)
# ... s.call("status") shows it under clients[].requests, with its progress
s.call("cancel", target=rid)   # or agent.cancel(rid) on a labelled view

Any language: the wire

Run python ghidra_session.py ensure once, then read default.json from the state directory for host, port and token. Connect, and speak one JSON object per line. The first request must be hello with the token. A real exchange, abridged:

> {"id": 1, "cmd": "hello", "client": "my-tool", "harness": "none", "clientVersion": "1.0.0", "token": "<token>"}
< {"id": 1, "ok": true, "result": {"protocol": 2, "minClient": "1.0.0", "hydraVersion": "12.3-hydra.dev", "ghidraVersion": "12.3", "installId": "c5e9d279befb866f", ...}, "ms": 0}
> {"id": 2, "cmd": "open_project", "path": "C:\\re\\Demo.gpr"}
< {"id": 2, "ok": true, "result": {"id": "p1", "name": "Demo", "writable": false, "revision": "0087e270279abab7", ...}, "ms": 0}
> {"id": 3, "cmd": "decompile", "program": "p1:/choice.exe", "address": "0x140001010", "maxChars": 120}
< {"id": 3, "ok": true, "result": {"name": "FUN_140001010", "signature": "undefined8 FUN_140001010(void);", "truncated": true, "c": "..."}, "ms": 301}

The reference has the full request and response shape.

Pin a release

Do not copy ghidra_session.py from main and hope the protocol still fits. on main · 12.3-hydra.2 Each release ships the client as hydra-client-<version>.zip next to the engine, with both in SHA256SUMS. Keep the engine and client of one release together.

python ghidra_session.py version prints the client's side:

{"client": "1.0.0", "protocol": 2, "release": "dev"}

hello, status and the endpoint file give the engine's: protocol, minClient, hydraVersion, ghidraVersion and installId. The Python client refuses a daemon whose protocol differs or whose minClient is newer than itself, with IncompatibleError (exit code 4 on the command line); status and stop still work, so you can stop it. A client in another language should apply the same two checks.

The rules behind the numbers: protocol changes only when a request or answer changes shape incompatibly. The client follows major.minor.patch. minClient rises only when an older client would misbehave. New fields and new optional request fields are additions and change none of them.

To upgrade: read the release's changelog section, stop the old daemon, put the new engine and client side by side, point your tool at them, then run version and ensure and check hello.

Checked against the engine on 2026-10-26. Every detail is in the README on GitHub.