Skip to main content
Glama
README.md
# hetzner-mcp

An MCP server that lets Claude operate a Hetzner estate: Cloud servers, DNS (Cloudflare and Hetzner DNS), hosts over SSH, Coolify, and multi-step recipes. Every write is a dry run until confirmed, protected servers refuse destructive calls, and every call is audited.

Status: v0.1.0. Built and tested against mocked APIs, a real SSH host, real Docker and a real Caddy. Not yet pointed at a live Hetzner project.

## Modules and tools (53 with everything configured)

| Module | Switched on by | Tools |
|---|---|---|
| fleet | always | `hz_fleet_overview`, `hz_host_list`, `hz_host_register`, `hz_host_remove` |
| cloud | `HCLOUD_TOKEN` | `hz_server_list`, `hz_server_get`, `hz_catalog`, `hz_action_status`, `hz_resource_list`, `hz_server_create`, `hz_server_power`, `hz_server_rebuild`, `hz_server_delete`, `hz_server_resize`, `hz_snapshot_create`, `hz_snapshot_delete`, `hz_backup_set`, `hz_firewall_create`, `hz_firewall_rules_set`, `hz_firewall_attach`, `hz_volume_create`, `hz_volume_action`, `hz_network_create`, `hz_network_attach`, `hz_ip_set_ptr`, `hz_floating_ip_assign`, `hz_ssh_key_add` |
| dns | `CLOUDFLARE_API_TOKEN` and/or `HCLOUD_TOKEN` | `dns_zone_list`, `dns_records_list`, `dns_record_set`, `dns_record_delete`, `dns_check` |
| host | `SSH_KEY` or `SSH_KEY_PATH` | `host_status`, `host_docker_ps`, `host_logs`, `host_file_read`, `host_caddy_routes`, `host_file_write`, `host_compose_action`, `host_caddy_route`, `host_run`, `host_bootstrap` |
| coolify | `COOLIFY_BASE_URL` + `COOLIFY_API_TOKEN` | `coolify_list`, `coolify_get`, `coolify_deployments`, `coolify_env_list`, `coolify_deploy`, `coolify_app_action`, `coolify_env_set` |
| recipes | SSH key (+ `HCLOUD_TOKEN` for new servers) | `recipe_new_app_server`, `recipe_deploy_stack` |
| audit | always | `audit_log` |

`hz_capabilities` is always present and says which modules are on and what each one needs.

## Safety model

- **Dry run by default.** Write tools return a plan unless called with `confirm: true`. Delete and rebuild also need `confirm_name` equal to the server name.
- **Protected resources.** A server is protected when it is in `PROTECTED_SERVERS`, has the Hetzner label `protected=true`, is marked `protected` in the host registry, or has Hetzner delete/rebuild protection on. Destructive calls on it are refused even with `confirm: true`.
- **Host registry.** SSH only reaches hosts listed in `config/hosts.json`. File reads and writes stay inside each host's `allowed_paths`. `.env` files return their keys, never values.
- **Audit log.** Every call (read, dry run, refused, executed) is appended to `data/audit.jsonl` with secrets masked.
- **Caddy edits** are fenced with markers, backed up, validated and rolled back on failure before a reload.

## Stack

Node 22, TypeScript (strict), `@modelcontextprotocol/sdk`, zod, ssh2, Express (Streamable HTTP), Biome, Vitest, pnpm, Docker.

## Quickstart (local, stdio)

```
pnpm install
cp .env.example .env            # fill in what you have
cp config/hosts.example.json config/hosts.json
pnpm build
```

Claude Desktop / Claude Code config:

```json
{
  "mcpServers": {
    "hetzner": {
      "command": "node",
      "args": ["C:\\path\\to\\hetzner-mcp\\dist\\index.js"],
      "env": { "HCLOUD_TOKEN": "…", "SSH_KEY_PATH": "C:\\Users\\you\\.ssh\\hetzner_mcp_ed25519", "HOSTS_FILE": "C:\\path\\to\\hetzner-mcp\\config\\hosts.json" }
    }
  }
}
```

Hosted (HTTP, behind Caddy on the app server): see [docs/RUNBOOK.md](docs/RUNBOOK.md).

## CI checks

```
pnpm typecheck && pnpm lint && pnpm test
```

CI runs the same three plus `pnpm build` and a gitleaks secret scan.

## Growing it

Add a folder under `src/modules/<id>/` exporting a `Module` (`status()` + `register()`), register tools through the `ToolKit` (`t.read` / `t.write`), and list it in `src/modules/index.ts`. The guard, audit and capability report come for free. See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).

## License

MIT