Skip to content

MCP server

New in 2.1.

Settings › MCP server lets an AI assistant — Claude Code, Codex, Antigravity, or any other Model Context Protocol client — read this node and, when you allow it, change it. Ask "why is the Jellyfin service down?" and the assistant can read the statistics, the URL probes and the logs; ask it to publish a service and it fills in the same wizard the UI does.

Every tool is a call to the node's own API, made inside the app. So an assistant can do nothing the UI could not, and nothing the UI would refuse: the same validation, the same haproxy -c before a change is stored, the same lock on a passive node, and a line in the log naming the client. Nothing it changes goes live until it calls apply_changes — the same Apply as the button.

It is off until you turn it on, and read-only until you allow changes.

Turning it on

  1. Open Settings › MCP server, tick Answer MCP clients at /mcp, and press Save. The pill in the header says on, read-only.
  2. Under Token, press Make a token. The card then shows its fingerprint and when it was made.
  3. Under Connect a client, pick your client and copy what it shows into it. See connecting a client.
  4. Tick Allow changes and Save again only if you want the assistant to change things, not only read them. See reading and changing.

The token is kept, so it can be shown again for the next client you set up:

  • Show the token puts it on the page, and fills it into the client text below; Hide the token takes it away again.
  • Copy copies it without showing it.
  • Replace the token makes a new one. The old one stops working at that moment, for every client using it, so each needs the new one.

A token made on 2.1.0-beta.1 was kept only as a hash and cannot be shown; the card says so. Press Replace the token once to get one that can.

The server answers at /mcp on the UI's own address — http://<node>:8080/mcp, or https://<name>/mcp once the UI is served through HAProxy. Use the HTTPS name: over plain HTTP the token crosses the network in the clear, exactly as a password would.

Connecting a client

The Connect a client card has a picker with each client below. It shows what that client needs, with the address the page was opened at and, once the token is shown, the token filled in; its Copy button copies the text with the token in it. Open the page through the virtual IP or the UI's public name, and the text points the client there, so it reaches whichever node is active (see in a cluster).

The same text is below, with https://proxy.example.net/mcp for the address and <token> for the token. Where it goes into a file, merge it with what is already there, then restart the program.

The token is in plain text

Each of these leaves the token in a configuration file on your computer. Keep those files out of repositories, screenshots and anything shared.

Run in a terminal. Every Claude Code session started after this has the tools, whatever folder it starts in.

claude mcp add --scope user --transport http haproxy-manager https://proxy.example.net/mcp --header "Authorization: Bearer <token>"

/mcp inside Claude Code then lists haproxy-manager with its tools.

Add to claude_desktop_config.json (Settings › Developer › Edit Config), then restart Claude Desktop. It connects through mcp-remote, a small bridge run with npx, so Node.js 18 or later is needed.

{
  "mcpServers": {
    "haproxy-manager": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://proxy.example.net/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer <token>"
      }
    }
  }
}

Add to ~/.codex/config.toml, then restart Codex. The CLI, the IDE extension and the app all read this file.

[mcp_servers.haproxy-manager]
url = "https://proxy.example.net/mcp"
http_headers = { "Authorization" = "Bearer <token>" }

Add to ~/.gemini/config/mcp_config.json (MCP Servers › Manage MCP Servers › View raw config), then restart Antigravity.

{
  "mcpServers": {
    "haproxy-manager": {
      "serverUrl": "https://proxy.example.net/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Run in a terminal.

gemini mcp add -s user --transport http --header "Authorization: Bearer <token>" haproxy-manager https://proxy.example.net/mcp

Add to ~/.openclaw/openclaw.json, then restart the OpenClaw gateway.

{
  "mcp": {
    "servers": {
      "haproxy-manager": {
        "url": "https://proxy.example.net/mcp",
        "transport": "streamable-http",
        "headers": {
          "Authorization": "Bearer <token>"
        }
      }
    }
  }
}

Add to ~/.cursor/mcp.json, then restart Cursor.

{
  "mcpServers": {
    "haproxy-manager": {
      "url": "https://proxy.example.net/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Add to mcp.json (MCP: Open User Configuration), then restart VS Code.

{
  "servers": {
    "haproxy-manager": {
      "type": "http",
      "url": "https://proxy.example.net/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Run in a terminal.

copilot mcp add --transport http --header "Authorization: Bearer <token>" haproxy-manager https://proxy.example.net/mcp

Formerly Windsurf. Add to ~/.config/devin/mcp_config.json, then restart Devin Desktop.

{
  "mcpServers": {
    "haproxy-manager": {
      "url": "https://proxy.example.net/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Add to settings.json (zed: open settings), then restart Zed.

{
  "context_servers": {
    "haproxy-manager": {
      "url": "https://proxy.example.net/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Run in a terminal.

cline mcp add haproxy-manager https://proxy.example.net/mcp --transport http --header "Authorization: Bearer <token>" --yes

Run in a terminal.

opencode mcp add haproxy-manager --global --url https://proxy.example.net/mcp --header 'Authorization=Bearer <token>'

Any client that can send a header to a Streamable HTTP server. It needs two things:

URL:     https://proxy.example.net/mcp
Header:  Authorization: Bearer <token>

A client that only starts local (stdio) servers needs a bridge between the two, such as mcp-remote as Claude Desktop uses it.

ChatGPT and claude.ai's connectors do not work. They sign in with OAuth only, which this server does not offer — it knows only its own token — and they connect from their provider's cloud, so the node would have to be reachable from the internet.

What the server implements is the part a tool server needs: initialize, ping, tools/list and tools/call, over POST /mcp, answered as plain JSON. There is no event stream, so GET /mcp answers 405. It accepts the protocol versions from 2024-11-05 to 2025-11-25.

A conversation

What it is like, in Claude Code with the server connected and Allow changes on. The services and numbers are an example; the tool calls are the ones the assistant makes, and the answers are put together from what they return. Wording varies from one conversation to the next.

list all my services on haproxy

haproxy-manager · list_services

You have four services, all HTTPS with HTTP redirected to them:

Service Address Forwards to Certificate
jellyfin https://jellyfin.example.com 192.168.1.20:8096 valid, 71 days
homeassistant https://homeassistant.example.com 192.168.1.30:8123 valid, 71 days
nextcloud https://nextcloud.example.com 192.168.1.40:80 valid, 71 days
grafana https://grafana.example.com 192.168.1.50:3000 valid, 71 days

None is paused, and nothing is waiting to be applied.

which service is busiest?

haproxy-manager · get_traffic (minutes: 1440)

Over the last 24 hours, jellyfin, by a wide margin:

  • jellyfin: 18,420 requests and 41.2 GB sent, most of it in the evening.
  • homeassistant: 9,870 requests, but only 310 MB; the app polls all day.
  • nextcloud: 1,530 requests, 2.1 GB; 14 of them were 5xx errors, all around 03:10.
  • grafana: 312 requests.

Want me to look at the nextcloud errors in the logs?

please rename my grafana service to dashboards

haproxy-manager · publish_service (service_id: grafana, name: dashboards, dry_run: true)
haproxy-manager · publish_service (service_id: grafana, name: dashboards)

Done. The Backend Pool is now dashboards, and the rule that routes to it follows the new name; its server and certificate are reused as they were. HAProxy accepted the result in the dry run first.

The address is still https://grafana.example.com: renaming the service does not change it. Say if you want the address changed too.

This is stored but not live yet; it goes live on Apply.

change the backend port for my homeassistant website

homeassistant forwards to 192.168.1.30:8123. Which port should it use?

8124

haproxy-manager · publish_service (service_id: homeassistant, target: http://192.168.1.30:8124, dry_run: true)
haproxy-manager · publish_service (service_id: homeassistant, target: http://192.168.1.30:8124)

Changed: homeassistant now forwards to 192.168.1.30:8124. Everything else about it is as it was: the address, the certificate, the health check.

Two changes are waiting now, this one and the rename. Shall I apply them?

yes

haproxy-manager · apply_changes
haproxy-manager · run_probes

Applied: HAProxy checked the new configuration and reloaded (in a cluster, the other nodes get it too). I probed every address afterwards and all four answer, https://homeassistant.example.com included, so the new port is the right one.

Things to notice:

  • It asks when the request leaves something out. "Change the port" did not say to what, so it asked rather than guessed.
  • A dry run comes first. The tools that have one show the lines a change becomes, and HAProxy's verdict on them, before anything is stored.
  • Nothing is live until Apply, and the server's instructions tell the assistant to ask before applying, restoring a snapshot or deleting. Claude Code also asks your permission before each tool it calls, unless you have allowed that tool for good.
  • The log names the client. Each change above is in the node's log as mcp:claude-code, like a change made in the UI is under your name.

The tools

The MCP server page links here rather than listing them. Reads tools work whenever the server is on; changes tools need Allow changes, except as a dry run.

Tool What it does Kind Dry run
get_status This node: HAProxy and Keepalived state, cluster role and virtual IPs, whether there are unapplied changes, certificates and their expiry, CrowdSec, updates reads
list_services Every published service: URLs, targets, health, certificate, sign-in, allow-lists, rules, HSTS, redirects, paused state reads
get_service One service, by its id, pool name or public URL reads
list_objects The objects of one collection under Advanced · HAProxy or ACME, as stored reads
get_settings One section of settings: haproxy, acme, cluster, crowdsec, notify or watchdog reads
get_haproxy_config The haproxy.cfg the stored configuration renders to — what Apply would write reads
check_config haproxy -c on that file, storing nothing reads
get_stats HAProxy's live counters per frontend, backend and server reads
get_traffic Requests, bytes and errors over time, for every pool or one reads
get_probes The latest outside check of every published URL reads
get_logs Recent log lines from the app, HAProxy, Keepalived, acme.sh or CrowdSec reads
get_cluster Every node as this one last saw it reads
list_history The configuration snapshots, newest first reads
get_history_diff What differs between a snapshot and now reads
get_vip_status Why this node does or does not hold the virtual IP: Keepalived's VRRP state and where it was read from, the virtual IPs and which it holds, the interface, keepalived -t on what Apply would write, and the journal's last lines. Ask each node for its own reads
check_certificate_setup Whether acme.sh is installed, runs and can store keys and certificates on this node, with the problem and the fix when it cannot reads
find_certificate Which configured certificate, if any, already covers a host name or URL — exactly or by a wildcard — with its status and days left. publish_service reuses it reads
list_recipes The recipes: ready-made settings for common applications, which can be passed to publish_service reads
run_probes Checks every published URL the way a browser would (DNS, connection, certificate, status) now instead of at the next minute, and returns the results as get_probes does. Useful right after apply_changes. Runs on the node holding the virtual IP or a standalone node; a passive node says so, since the names lead to the virtual IP. Changes no configuration reads
get_certificate_log acme.sh's output from a certificate's last issue or renewal reads
publish_service Publish a service — forwarded or a redirect — or edit one by service_id; fields left out keep their values changes yes
pause_service Pause or resume a service changes
delete_service Remove a service and the objects only it used changes
save_rule Create or edit a rule, with its conditions and listeners changes yes
save_condition Create or edit a condition, optionally testing a sample request against it changes yes
request_certificate Create a certificate for some names and issue it through ACME changes yes
issue_certificate Issue or renew an existing certificate now changes
renew_certificates What the daily renewal does, now: every auto-renewing certificate that is due is renewed. Uploaded certificates are never renewed. Runs on the node holding the virtual IP changes
create_object Add an object to a collection changes haproxy/ only
update_object Replace an object with a changed copy of it changes haproxy/ only
delete_object Remove one object from a collection changes
update_settings Change haproxy or acme settings, only the keys given; refused if HAProxy would not accept the result changes
restore_history Put the configuration back to a snapshot changes
apply_changes Write haproxy.cfg, validate, reload and push to the other nodes — the step that makes changes live changes

Each tool carries the MCP hints a client uses to decide what to ask you about: the reading ones are marked read-only, and delete_service, update_object, delete_object, restore_history and apply_changes destructive.

There is no tool to upload or download a certificate or its private key, none for the login, the API key, the peers or the MCP server's own settings and token, and list_objects reaches only the HAProxy and ACME collections.

Reading and changing

With Allow changes off, every reading tool works and every changing tool is refused, with a message saying where the switch is. The tools with a dry run still answer when given dry_run: true, and store nothing:

  • publish_service, save_rule, save_condition and request_certificate return the configuration lines the change would produce and HAProxy's verdict on them.
  • create_object and update_object return the lines the object renders to in haproxy.cfg and haproxy -c on the whole file with it in. That works for the haproxy/ collections only; for an acme/ collection, which renders nothing into haproxy.cfg, the dry run is refused.

That is the way to let an assistant draft a change for you to make. A dry_run given to a changing tool that has none is not a dry run: it is refused like any other change while changes are off.

With it on, the assistant can publish, edit and delete, and Apply. A change is stored first and goes live only at apply_changes, and get_status says whether anything is waiting, so the order is the one the UI follows: change, check, apply. The server tells the client so when it connects, and asks it to prefer a dry run first where there is one.

In a cluster

The cluster behaves as one server:

  • The settings and the token are the same everywhere. Saving on the MCP server page, or making a token, copies the switches and the token to every other node, over the peer link with each node's API key. A node that did not take it is named on the page — change it there too, or save again once it answers. A node added later gets them the next time you save.
  • A change asked of a passive node is made on the active one. A changing tool called on a node that does not hold the virtual IP is passed to the node that does, and the answer starts with (made on proxy1, which holds the virtual IP; this node is passive). If no node answers as the one holding it, the change is refused rather than made somewhere it would be overwritten. renew_certificates goes the same way, since only that node renews.
  • Reading stays local. A reading tool answers from the node asked, so get_status or get_vip_status on a passive node describes that node. run_probes is refused there: the published names lead to the virtual IP, so only the node holding it probes them.

Point the client at the virtual IP, or at the UI's public name, so it always reaches whichever node is active.

Security

  • Its own token. /mcp does not accept the API key or a UI session, only the MCP token, sent as Authorization: Bearer. A request is checked against the token's SHA-256 hash; the token itself is kept beside it in config.json, as the API key is, so the page can show it again. Anyone signed in as the administrator, or holding the API key, can have it shown, and showing it is logged. The page shows an eight-character fingerprint and the date it was made. A token handed to an assistant can be replaced without touching the API key the cluster depends on.
  • Secrets are kept back. Anything whose name looks like a secret — passwords, tokens, keys, credentials, secrets, salts, hashes, TOTP — is returned as [redacted], at any depth. Sending [redacted] back in update_object keeps the stored value, so an object read and written back does not lose its DNS credentials or client secret. No tool returns the MCP token.
  • No other site's page can use it. A request carrying an Origin header for any host other than the one it was sent to is refused, so a web page cannot reach a node through a rebound DNS name.
  • Everything is in the log. Each tool call is logged with the client's name and its arguments (without secret-looking ones), and every change it makes is in the audit log as mcp:<client name> — the name the client gave when it connected, such as mcp:claude-code. A change forwarded from a passive node adds via <address>. A request without a valid token is logged as a warning, and so is making a new token.
  • Off and read-only by default, and a node's settings never travel in a backup or a history snapshot: they are per node, copied only by the page itself.
  • What a tool returns goes to the model's provider. The assistant reads the results to answer you, so service names, addresses, log lines and the rendered configuration leave the node for wherever the model runs. Turn the server on only where that is acceptable, and turn it off again when it is not in use.