{"slug": "henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings", "title": "Henka: Multi-tenant MCP server for structured, semantics-aware code refactorings", "summary": "Henka, an open-source multi-tenant Model Context Protocol (MCP) server, enables AI agents to perform structured, semantics-aware code refactorings across Java, Rust, and TypeScript/JavaScript projects using each language's native toolchain, such as Eclipse JDT for Java and rust-analyzer for Rust. The server, which supports stdio and streamable HTTP transports, bundles language servers and offers worktree-aware indexing, with a Docker image available for deployment. The project's HTTP transport is unauthenticated, so binding beyond loopback exposes registered projects to anyone who can reach the port.", "body_md": "Structured, semantics-aware code refactorings for AI agents, spoken over [MCP](https://modelcontextprotocol.io) — real refactorings computed by the language's own toolchain, not text munging.\n\n**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`jj`\n\nworkspaces. A refactoring lands in whichever working copy you name; the others are untouched.\n\nThe full specification is in [ docs/SPEC.md](/martint/henka/blob/main/docs/SPEC.md).\n\nBuild 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):\n\n``` php\ncargo xtask build          # -> target/release/henka\n```\n\nRun it over **stdio** for a single local client (the default):\n\n```\nhenka\n```\n\n…or over **streamable HTTP** to host it for one or more clients at `/mcp`\n\n:\n\n```\nhenka --transport http --bind 127.0.0.1:8181\n```\n\nFlags (each has effect only where noted):\n\n| Flag | Purpose |\n|---|---|\n`--transport stdio|http` |\nHow clients connect. Default `stdio` . |\n`--bind <addr>` |\nAddress for `--transport http` . Default `127.0.0.1:8181` . |\n`--config <path>` |\nProject registry file. Default `$XDG_CONFIG_HOME/henka/projects.toml` . |\n`--allowed-host <host>` |\nExtra `Host` value accepted over HTTP, beyond the loopback defaults. Repeatable, or set `HENKA_MCP_ALLOWED_HOST` to a space-separated list. |\n\nEnvironment mirrors and discovery: `HENKA_DATA`\n\n(single root for all persistent state — the registry and the per-repository indexes), `HENKA_CONFIG`\n\n(registry path, overrides `HENKA_DATA`\n\nfor the registry alone), `JDTLS_HOME`\n\n/ `HENKA_JDTLS_BUNDLE`\n\n(Java language server + bundle), `JAVA_HOME`\n\n(JVM to launch it with), `HENKA_LOG`\n\n(log filter; logs go to stderr).\n\n**The HTTP transport is unauthenticated.** Binding beyond loopback (e.g. `--bind 0.0.0.0:8181`\n\n) 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.\n\nA prebuilt image bundles Henka and the language servers it drives (no host toolchain needed). Copy the example compose and env files and point `HENKA_WORKSPACES_DIR`\n\nat a host directory of working copies (mounted at `/workspaces`\n\n); each working copy under it is auto-registered as a project, so a client can operate on it straight away:\n\n```\ncp docker-compose.yml.example docker-compose.yml\ncp .env.example .env          # then set HENKA_WORKSPACES_DIR\nHENKA_UID=$(id -u) HENKA_GID=$(id -g) \\\n  docker compose up -d        # serves MCP at http://127.0.0.1:8181/mcp\n```\n\nSetting `HENKA_UID`\n\n/`HENKA_GID`\n\nruns 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.\n\nThe container sees its working copies under `/workspaces`\n\n, 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`\n\n, an absolute `file`\n\n) by `host=container`\n\nprefix, and the compose file wires that up from your `HENKA_WORKSPACES_DIR`\n\nand the `/workspaces`\n\nmount automatically. For more mounts, append extra `host=container`\n\nrewrites in `HENKA_PATH_MAP`\n\n.\n\nSee [ docs/deploying.md](/martint/henka/blob/main/docs/deploying.md) for configuration, the GHCR image, and security notes.\n\nA client registers a project — a local source tree, typically a `jj`\n\nor `git`\n\nrepository — and Henka detects its languages and persists the registration. Source is never copied or moved; Henka operates on the tree in place.\n\nEach operation is one MCP tool. A call names the `project`\n\n, 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`\n\nto apply.\n\nWhen a project spans several working copies, a call may also name a `workspace`\n\n(a git worktree or `jj`\n\nworkspace path, or it's inferred from an absolute `file`\n\n). 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.\n\nHenka exposes a handful of tenancy tools — `register_project`\n\n, `unregister_project`\n\n, `list_projects`\n\n, `project_status`\n\n, `list_operations`\n\n— plus one tool per operation, routed to the backend for the target file's language. `project_status`\n\nalso 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`\n\n, `find-usages`\n\n, `change-signature`\n\n, `extract-variable`\n\n, `extract-constant`\n\n, `extract-field`\n\n, `extract-method`\n\n, `inline`\n\n, and `organize-imports`\n\n; Rust offers `rename`\n\n, `find-usages`\n\n, `extract-variable`\n\n, `extract-constant`\n\n, `extract-function`\n\n, and `inline`\n\n; TypeScript/JavaScript offers `rename`\n\n, `find-usages`\n\n, `extract-constant`\n\n, and `extract-function`\n\n.\n\nWire it into [Claude Code](https://claude.com/claude-code) over **stdio** (no network, no auth surface):\n\n```\nclaude mcp add henka -- /abs/path/to/target/release/henka\n```\n\n…or over **HTTP**. Note the streamable-HTTP transport rejects non-loopback `Host`\n\nheaders as a DNS-rebinding guard, so a client reaching the server under another name — e.g. from a container as `host.docker.internal`\n\n— needs that host allowed:\n\n```\nhenka --transport http --bind 0.0.0.0:8181 --allowed-host host.docker.internal\nclaude mcp add --transport http henka http://host.docker.internal:8181/mcp\n```\n\nThen 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.\n\nA Cargo workspace of focused crates:\n\n| Crate | Purpose |\n|---|---|\n`henka-core` |\nLanguage-agnostic core: the project registry, the operation and workspace-edit model, and VCS / repository identity. |\n`henka-lsp` |\nA minimal LSP client plus the reusable `LspSession` (open/index/overlay/sync) every backend shares. |\n`henka-lang-java` |\nThe Java provider: launches and drives Eclipse JDT LS (`jdtls` ) and contributes the Java operations. |\n`henka-lang-rust` |\nThe Rust provider: launches and drives rust-analyzer and contributes the Rust operations. |\n`henka-lang-ts` |\nThe TypeScript/JavaScript provider: launches and drives typescript-language-server (serving both languages) and contributes their operations. |\n`henka-server` |\nThe MCP server binary: the dynamic tool catalog, request dispatch, and the stdio / HTTP transports. |\n`xtask` |\nBuild automation, invoked as `cargo xtask` . |\n\nThe Java backend additionally relies on a small OSGi **delegate-command bundle** (`jdtls-bundle/`\n\n) compiled against jdtls, which unlocks parameterized refactorings like change-signature.\n\n```\ncargo xtask build          # full build: jdtls (if missing) + bundle + release binary\ncargo test                 # unit and mock-backed tests\ncargo test -p henka-lang-java -- --ignored   # integration tests that launch a real jdtls\n```\n\n`cargo xtask jdtls`\n\nand `cargo xtask bundle`\n\nrun the jdtls fetch and the bundle compile on their own.\n\nApache-2.0.", "url": "https://wpnews.pro/news/henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings", "canonical_source": "https://github.com/martint/henka", "published_at": "2026-08-23 14:53:30+00:00", "updated_at": "2026-08-23 15:14:25.681834+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents", "artificial-intelligence"], "entities": ["Henka", "MCP", "Eclipse JDT", "rust-analyzer", "typescript-language-server", "Docker"], "alternates": {"html": "https://wpnews.pro/news/henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings", "markdown": "https://wpnews.pro/news/henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings.md", "text": "https://wpnews.pro/news/henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings.txt", "jsonld": "https://wpnews.pro/news/henka-multi-tenant-mcp-server-for-structured-semantics-aware-code-refactorings.jsonld"}}