Structured, semantics-aware code refactorings for AI agents, spoken over MCP — real refactorings computed by the language's own toolchain, not text munging.
- Refactorings, not string edits. Rename, extract, inline, change-signature, organize-imports, find-usages — computed by the language's real compiler view (Eclipse JDT for Java), so they hold across files and overloads. Every edit comes back as a diff you can preview before anything touches disk.
- One server, many projects. Henka is multi-tenant: register repositories and operate on them in place. Operations are contributed per language — Java (via Eclipse JDT), Rust (via rust-analyzer), and TypeScript/JavaScript (via typescript-language-server) — and the language servers are bundled, so there's nothing extra to install.
- Worktree- and workspace-aware. One warm index per repository, shared across git worktrees and
jjworkspaces. A refactoring lands in whichever working copy you name; the others are untouched.
The full specification is in docs/SPEC.md.
Build the release binary (this also bundles the language servers it drives — fetching rust-analyzer, the Java language server, and typescript-language-server, and building the Java delegate-command bundle):
cargo xtask build # -> target/release/henkaRun it over stdio for a single local client (the default):
henka…or over streamable HTTP to host it for one or more clients at /mcp:
henka --transport http --bind 127.0.0.1:8181Flags (each has effect only where noted):
| Flag | Purpose |
|---|---|
--transport stdio|http |
How clients connect. Default stdio. |
--bind <addr> |
Address for --transport http. Default 127.0.0.1:8181. |
--config <path> |
Project registry file. Default $XDG_CONFIG_HOME/henka/projects.toml. |
--allowed-host <host> |
Extra Host value accepted over HTTP, beyond the loopback defaults. Repeatable, or set HENKA_MCP_ALLOWED_HOST to a space-separated list. |
Environment mirrors and discovery: HENKA_DATA (single root for all persistent state — the registry and the per-repository indexes), HENKA_CONFIG (registry path, overrides HENKA_DATA for the registry alone), JDTLS_HOME / HENKA_JDTLS_BUNDLE (Java language server + bundle), JAVA_HOME (JVM to launch it with), HENKA_LOG (log filter; logs go to stderr).
The HTTP transport is unauthenticated. Binding beyond loopback (e.g. --bind 0.0.0.0:8181) exposes every registered project to anyone who can reach the port — wrong for anything shared. Keep it on loopback, or front it with a reverse proxy that terminates auth.
A prebuilt image bundles Henka and the language servers it drives (no host toolchain needed). Copy the example compose and env files, point HENKA_WORKSPACES_DIR at a host directory of working copies (mounted at /workspaces), and register each as a project by its in-container path:
cp docker-compose.yml.example docker-compose.yml
cp .env.example .env # then set HENKA_WORKSPACES_DIR
HENKA_UID=$(id -u) HENKA_GID=$(id -g) \
docker compose up -d # serves MCP at http://127.0.0.1:8181/mcpSetting HENKA_UID/HENKA_GID runs the container as you, so the edits Henka writes into your repos are owned by you and not root; leave them unset to run as root.
The container sees its working copies under /workspaces, not at their host paths, so a client that speaks host paths would otherwise have to register projects by their in-container path. It doesn't need to: Henka rewrites caller-supplied paths (a project root, a workspace, an absolute file) by host=container prefix, and the compose file wires that up from your HENKA_WORKSPACES_DIR and the /workspaces mount automatically. For more mounts, append extra host=container rewrites in HENKA_PATH_MAP.
See docs/deploying.md for configuration, the GHCR image, and security notes.
A client registers a project — a local source tree, typically a jj or git repository — and Henka detects its languages and persists the registration. Source is never copied or moved; Henka operates on the tree in place.
Each operation is one MCP tool. A call names the project, a target (a file with a position, a selection, or the whole file), and any operation-specific parameters. Edit operations default to a preview: they return the diff each file would receive and touch nothing. Pass dry_run=false to apply.
When a project spans several working copies, a call may also name a workspace (a git worktree or jj workspace path, or it's inferred from an absolute file). Henka keeps one warm index per repository, overlays that working copy's content onto it, computes the refactoring, and writes the result into that working copy — so a dozen worktrees share one index instead of each paying a cold re-analysis.
Henka exposes a handful of tenancy tools — register_project, unregister_project, list_projects, project_status, list_operations — plus one tool per operation, routed to the backend for the target file's language. project_status also reports the version-control state Henka reads a project at (revision, branch, repo root, dirty), so an agent can confirm Henka's checkout matches the working copy it is editing before trusting line/character coordinates against it. Java offers rename, find-usages, change-signature, extract-variable, extract-constant, extract-field, extract-method, inline, and organize-imports; Rust offers rename, find-usages, extract-variable, extract-constant, extract-function, and inline; TypeScript/JavaScript offers rename, find-usages, extract-constant, and extract-function.
Wire it into Claude Code over stdio (no network, no auth surface):
claude mcp add henka -- /abs/path/to/target/release/henka…or over HTTP. Note the streamable-HTTP transport rejects non-loopback Host headers as a DNS-rebinding guard, so a client reaching the server under another name — e.g. from a container as host.docker.internal — needs that host allowed:
henka --transport http --bind 0.0.0.0:8181 --allowed-host host.docker.internal
claude mcp add --transport http henka http://host.docker.internal:8181/mcpThen point the agent at a registered project and ask for a rename, an extract, or a usage search; previews come back as diffs, so the agent can look before it applies.
A Cargo workspace of focused crates:
| Crate | Purpose |
|---|---|
henka-core |
Language-agnostic core: the project registry, the operation and workspace-edit model, and VCS / repository identity. |
henka-lsp |
A minimal LSP client plus the reusable LspSession (open/index/overlay/sync) every backend shares. |
henka-lang-java |
The Java provider: launches and drives Eclipse JDT LS (jdtls) and contributes the Java operations. |
henka-lang-rust |
The Rust provider: launches and drives rust-analyzer and contributes the Rust operations. |
henka-lang-ts |
The TypeScript/JavaScript provider: launches and drives typescript-language-server (serving both languages) and contributes their operations. |
henka-server |
The MCP server binary: the dynamic tool catalog, request dispatch, and the stdio / HTTP transports. |
xtask |
Build automation, invoked as cargo xtask. |
The Java backend additionally relies on a small OSGi delegate-command bundle (jdtls-bundle/) compiled against jdtls, which unlocks parameterized refactorings like change-signature.
cargo xtask build # full build: jdtls (if missing) + bundle + release binary
cargo test # unit and mock-backed tests
cargo test -p henka-lang-java -- --ignored # integration tests that launch a real jdtlscargo xtask jdtls and cargo xtask bundle run the jdtls fetch and the bundle compile on their own.
Apache-2.0.