Haify MCP server (for AI agents)¶
haify-mcp (in cmd/mcp) is a
Model Context Protocol server for the Haify
controller. An AI agent connects to it and uses its tools to inspect and
operate the cluster. It calls the controller's gRPC API, the same API haify
uses. It runs in three ways:
| Command | Transport | Who may call what |
|---|---|---|
haify-mcp |
stdio, for a client on the same machine | every tool, or only reads with --read-only |
haify-mcp serve |
HTTP at /mcp, for remote clients |
decided by the role of each client's token |
haify-mcp k8s |
stdio | the Kubernetes tools (haify_k8s_*), which talk to a Kubernetes API server, not to the controller |
Install¶
make install-mcp # runs `make build` (needs Node.js for the web UI), copies bin/haify-mcp to /usr/local/bin
# or just the binary:
go build -o ~/.local/bin/haify-mcp ./cmd/mcp
Local server (stdio)¶
claude mcp add haify -- haify-mcp --controller 10.0.0.250:3374
To set it up for one project instead, put it in a .mcp.json file. Claude Code
loads that file from the directory it starts in:
{
"mcpServers": {
"haify": {
"type": "stdio",
"command": "haify-mcp",
"args": ["--controller", "10.0.0.250:3374"],
"env": {}
}
}
}
Replace the address with your cluster's. Claude Code expands environment
variables in .mcp.json, so "${HAIFY_CONTROLLER_ADDR:-127.0.0.1:3374}" also
works there. The client reads the file only when it starts, so restart it after
you change the file.
Which address to use:
- With Self-HA enabled, use the VIP. On failover the controller moves to
another node, and the VIP moves with it.
haify ha self statusprints it. - Without Self-HA, use the controller node's own address.
- On the controller node itself, leave the flag out: the default
127.0.0.1:3374reaches it.
Flags:
--controller, -c: controllerhost:port(default127.0.0.1:3374).--token: the API token, needed when the controller has[auth]or[rbac]enabled. Without the flag, haify-mcp looks inHAIFY_TOKEN, then~/.haify/token, then/etc/haify/token.--tls,--tls-ca,--tls-cert,--tls-key,--tls-server-name,--tls-insecure: use these to reach a controller that has[tls]enabled. They mean the same as in thehaifyCLI and read the sameHAIFY_TLS*environment variables (user guide §17).--read-only: register only the read-only tools.--allow NAME[,NAME]: register these mutating tools as well, for example--allow haify_ha_evict. It turns on--read-only, so every other tool that changes the cluster stays off. The server refuses to start if a name is not a mutating tool, so a typo cannot silently leave a tool out.--debug: debug logging on stderr. stdout is kept for the protocol.
Kubernetes tools¶
haify-mcp k8s serves three tools for databases on Kubernetes whose data
lives on a Haify volume: haify_k8s_app_list, haify_k8s_app_create and
haify_k8s_app_delete.
Delete removes the Deployment and the Service. It keeps the volume claim and
the password Secret, so creating the app again under the same name picks up
the same data. Set delete_data to delete those too. Delete only touches
objects that haify_k8s_app_create made.
It finds the cluster through --kubeconfig (or HAIFY_KUBECONFIG), or uses
the in-cluster config when it runs inside a pod. It also takes --read-only,
--allow and --debug, which work as in the main server.
Remote server (haify-mcp serve)¶
haify-mcp serve serves the same tools over streamable HTTP at /mcp. Every
call there needs a token.
haify-mcp token create --name laptop --role read --url https://mcp.example.com/mcp
haify-mcp serve --listen 0.0.0.0:43871 --public-url https://mcp.example.com
token create prints the secret once, together with the claude mcp add
command that uses it. --expires 720h sets how long the token lasts. The
default, 0, means it never expires.
Roles¶
Each token has a role. The role decides which tools exist on that connection at all. A tool outside the role is not registered, so it cannot be called, whatever the client sends.
| Role | Tools |
|---|---|
read |
the read-only tools: lists, status, health, diagnose, inspection reports and runs (haify_inspect_run changes nothing on the cluster), events, logs, audit, gateway exports/LUNs/ACLs/CHAP settings, runbooks |
operate |
plus every mutating tool that is not marked destructive (the middle column below) |
admin |
plus the destructive ones: delete, remove, unregister, restore, stop, evict, drain, renumber, role change, unmount, filesystem creation, thin-pool conversion, moving a volume to another pool, promoter TOML sync, schedule delete, Self-HA enable/disable, DR failback, database app failover, and adding or removing gateway exports, SMB shares, LUNs, NVMe namespaces and allow-list entries |
A tool is admin if it can lose data, take a volume away from the node that
serves it (role change, unmount, gateway stop, evict, drain, app failover), or
stop future snapshots or backups.
operate tools can still interrupt replication for a moment.
haify_resource_set_options, haify_resource_tls, haify_wan_repair,
haify_wan_set_endpoint and haify_wan_tune may take replication links down and bring them back,
one link at a time, while the Primary keeps serving. This role can also detach
a diskless client (haify_resource_detach_diskless), which ends that node's
access to the volume.
--max-role read|operate|admin caps every token on the main listener,
whatever role the token has. The default is admin, which caps nothing.
--admin-listen ADDR opens a second listener that --max-role does not cap.
It is for operators on the local network: an admin token there gets every
tool, while the listener that a reverse proxy points at stays capped. The
second listener accepts only tokens made with token create, sent as a
bearer header. It has no OAuth endpoints, and it refuses the access tokens
that ChatGPT or claude.ai got through OAuth on the main listener. Never put it
behind the proxy. Bind it to a port the proxy does not forward, for example
--listen 0.0.0.0:43871 --max-role operate --admin-listen 0.0.0.0:43872.
Tokens¶
Tokens are stored as SHA-256 hashes in /var/lib/haify/mcp/tokens.json.
--tokens or HAIFY_MCP_TOKENS changes the path. With Self-HA,
/var/lib/haify is the haify-meta mount, so the token store moves with the
controller. Run haify-mcp token commands on the node where haify-meta is
mounted.
haify-mcp token list and haify-mcp token revoke <name|id> work while the
server runs. The server re-reads the file when it changes, so a revoked token
is refused on its next request, even inside a session that is already open.
Revoking a token also revokes every OAuth token issued under it.
If the store is empty, the server still starts. It logs a warning and refuses every request until you create a token.
Clients¶
Claude Code sends the token as a header:
claude mcp add --transport http haify https://mcp.example.com/mcp \
--header "Authorization: Bearer haifymcp_..."
ChatGPT and claude.ai add a server by its URL and then run OAuth. The server
offers OAuth only when it is started with --public-url. The client finds the
endpoints and registers itself. On the authorization page you paste a token
made with token create. The client then gets an access token that is valid
for one hour and a refresh token that is valid for 30 days. They carry the
pasted token's role, capped by --max-role, or a lower role if the client
asked for a smaller scope. Only S256 PKCE is accepted. Redirect URIs must be
https, or http to a loopback address.
Deployment¶
Serve it over HTTPS. Either give it --tls-cert/--tls-key, or let a
reverse proxy terminate TLS. With a proxy, bind the server to a private
address and pass --trust-proxy. The server then reads the real client
address from X-Forwarded-For. The failed-login limit needs that address: it
locks an address out after 10 failed attempts within 5 minutes.
GET /healthz answers ok without a token.
serve has its own flags for reaching the controller. --controller
(default 127.0.0.1:3374) is the address. --controller-token is the token
and falls back to the same places as --token above. For a controller with
[tls] enabled, use --controller-tls, --controller-tls-ca,
--controller-tls-cert, --controller-tls-key,
--controller-tls-server-name and --controller-tls-insecure. These also
fall back to the HAIFY_TLS* variables. Do not mix them up with
--tls-cert/--tls-key, which are the HTTPS certificate of this server
itself.
Every tool call is logged with the token's name and role. The arguments are not logged, because some of them carry secrets:
journalctl -u haify-mcp-http | grep 'tool call'
To make the server move with the controller on failover:
- Copy
haify-mcpto/opt/haify/bin/and installconfigs/haify-mcp-http.serviceon every node. Leave the unit disabled. It runs/opt/haify/bin/haify-mcp serve --listen 0.0.0.0:43871with the token store on the Self-HA mount, and reads your site's extra flags fromHAIFY_MCP_ARGSin/var/lib/haify/mcp/haify-mcp.env. - Add
haify-mcp-http.serviceto the haify-meta promoter's start list. If Self-HA is not enabled yet, list it in[self_ha] extra_servicesincontroller.tomlbefore you runha self enable. On a cluster that already runs Self-HA, add it to/etc/drbd-reactor.d/haify-ha-haify-meta.tomlon every node. List it in that same setting as well, so the controller's stored copy of the promoter matches and a later sync does not drop it.
Runbooks¶
A runbook says in which order to call the tools for a task, and what breaks
halfway if you use another order. The runbooks are built into the binary from
pkg/mcpserver/runbooks/*.md and are served in two ways:
- as MCP prompts, which Claude Code offers as slash commands. Each takes an
optional
targetargument (the resource or node). - through the read-only tool
haify_runbook. Without a name it lists the runbooks; with a name it returns that one. ChatGPT, claude.ai and the AI Copilot reach the runbooks this way.
| Runbook | For | Needs |
|---|---|---|
add-replica |
another full copy of a resource on another node | operate |
dr-failback |
a WAN resource back to its primary site after a DR failover | admin |
grow-volume |
a larger volume, and the filesystem on it | operate |
planned-switchover |
haify-meta or a gateway moved to another node on purpose | admin |
reboot-node |
a node taken out of service and brought back | admin |
renumber-nodes |
nodes whose IP addresses changed | admin |
verify-and-repair |
replicas compared block by block, and differences repaired | operate |
Tools¶
| Area | read | operate adds | admin adds |
|---|---|---|---|
| Cluster, nodes | haify_node_list, haify_node_health_check, haify_diagnose, haify_inspect_report, haify_inspect_run, haify_event_list, haify_log_list, haify_audit_list, haify_ocf_agent_list, haify_ocf_agent_metadata, haify_notify_channel_list, haify_replication_tls_status, haify_runbook |
haify_node_register, haify_node_set_labels, haify_node_set_replication_paths, haify_node_undrain, haify_notify_channel_test |
haify_node_drain, haify_node_unregister, haify_node_set_address |
| LVM pools | haify_pool_list, haify_node_disks, haify_pool_disks, haify_storage_jobs |
haify_pool_create, haify_pool_add_disk, haify_pool_add_cache, haify_pool_remove_disk, haify_pool_replace_disk, haify_pool_trim |
haify_pool_delete, haify_pool_remove_cache, haify_pool_convert_thin |
| ZFS | haify_zfs_pool_list |
haify_zfs_volume_create, haify_zfs_volume_resize, haify_zfs_dataset_create, haify_zfs_snapshot_clone |
haify_zfs_pool_delete, haify_zfs_dataset_delete |
| Resources, volumes | haify_resource_list, haify_resource_status |
haify_resource_create, haify_resource_adopt, haify_resource_add_volume, haify_resource_resize_volume, haify_resource_set_options, haify_resource_mount, haify_resource_dual_primary, haify_resource_repair, haify_resource_verify, haify_resource_tls, haify_resource_set_labels |
haify_resource_delete, haify_resource_remove_volume, haify_resource_move_volume, haify_resource_create_filesystem, haify_resource_set_role, haify_resource_unmount |
| Replicas, WAN | haify_wan_sizing |
haify_resource_add_replica, haify_resource_attach_diskless, haify_resource_detach_diskless, haify_resource_set_tiebreaker, haify_resource_add_dr, haify_wan_repair, haify_wan_set_endpoint, haify_wan_tune |
haify_resource_remove_replica, haify_resource_dr_failback |
| Profiles | haify_resource_profile_list, haify_resource_profile_get, haify_resource_profile_max_size |
haify_resource_profile_create, haify_resource_profile_set_options, haify_resource_profile_adjust, haify_resource_set_profile |
haify_resource_profile_delete |
| Snapshots | haify_snapshot_list, haify_snapshot_schedule_list |
haify_snapshot_create, haify_snapshot_schedule_create |
haify_snapshot_delete, haify_snapshot_restore, haify_snapshot_schedule_delete |
| Backups | haify_backup_list, haify_backup_target_list, haify_backup_schedule_list |
haify_backup_create, haify_backup_import, haify_backup_schedule_create, haify_resource_ship |
haify_backup_delete, haify_backup_restore, haify_backup_target_delete, haify_backup_schedule_delete |
| Gateways | haify_gateway_list, haify_gateway_get, haify_nfs_export_list, haify_iscsi_lun_list, haify_iscsi_initiator_list, haify_iscsi_chap_get, haify_nvme_namespace_list, haify_nvme_host_list, haify_gateway_smb_users |
haify_gateway_create_nfs, haify_gateway_create_iscsi, haify_gateway_create_nvme, haify_gateway_create_smb, haify_gateway_start, haify_iscsi_chap |
haify_gateway_stop, haify_gateway_delete, haify_nfs_exports, haify_iscsi_luns, haify_iscsi_initiators, haify_nvme_namespaces, haify_nvme_hosts, haify_gateway_smb_shares |
| Database apps | haify_app_list, haify_app_status |
haify_app_create, haify_app_snapshot |
haify_app_failover, haify_app_delete |
| HA, Self-HA | haify_ha_list, haify_ha_status, haify_ha_promoter_status, haify_ha_get_toml, haify_self_ha_status |
haify_ha_create |
haify_ha_evict, haify_ha_delete, haify_ha_sync_toml, haify_self_ha_enable, haify_self_ha_disable |
haify_nfs_exports, haify_iscsi_luns, haify_iscsi_initiators,
haify_nvme_namespaces and haify_nvme_hosts add and remove entries, so they
need admin. They still accept the list action, but the *_list tools give
the same answer at read.
haify_iscsi_chap sets one-way CHAP and needs operate. Mutual CHAP is
refused. It still accepts get, which haify_iscsi_chap_get also answers at
read. Neither tool returns the password.
What has no tool¶
- Reading the password that
haify app creategenerates.haify_app_createtells you where the password is kept (root-only, on the app's volume) instead of returning it, because whatever called the tool records its result. - Commands that take a secret, because whatever calls a tool records its
arguments:
backup target add(storage credentials),channel add(webhook URLs and signing keys),rbac. The one exception ishaify_iscsi_chap, which sets a CHAP password. The server does not log tool arguments and never returns the password. resource dr-failover. It force-promotes the DR node and loses the writes still in the WAN buffer. That decision is taken at the CLI, which asks for--yes.replication-tls setup. It installs a CA into each node's system trust store.inspect list(haify_inspect_reportreads the newest report, or one by id),backup schedule run,channel delete,event watch(a stream;haify_event_listreads the history) andgateway nfs mount(it mounts on the machine running the CLI).