HAProxy Dynamic Load Balancer Management Suite

Runtime management of HAProxy without restarts. Provides an MCP (Model Context Protocol) interface for dynamic domain/server management with zero-reload operations.

Key Features

  • Zero-reload domain management using HAProxy map files and pool backends
  • Dynamic server management with auto-persistence
  • SSL/TLS certificate management with zero-downtime deployment via Runtime API
  • Pool sharing for multiple domains pointing to the same backend
  • HTTP/3 (QUIC) support alongside HTTP/1.1 and HTTP/2
  • 29 MCP tools across domain, server, health, monitoring, config, and certificate management
  • IPv4 and IPv6 support

Architecture

┌──────────────────────────────────────────────────┐
│ HAProxy (host network)                           │
│ TCP 80/443 (HTTP/1.1, HTTP/2) + UDP 443 (HTTP/3) │
│ Runtime API: localhost:9999                      │
│ MCP Proxy: mcp.inouter.com → localhost:8000      │
└──────────────┬───────────────────────────────────┘
               │
         ┌─────▼─────┐
         │ MCP       │
         │ Server    │
         │ :8000     │
         └───────────┘

Domain Routing Flow

Request → HAProxy Frontend
    ↓
domains.map lookup (Host header)
    ↓
pool_N backend (N = 1-100)
    ↓
Server (from servers.json, auto-restored on startup)

Pool-Based Routing

  • Domains map to pools: example.compool_N
  • 100 pools available: pool_1 to pool_100
  • Each pool has 10 server slots (pool_N_1 to pool_N_10)

Prerequisites

  • HAProxy 3.x (running as Podman Quadlet container)
  • Python 3.11+
  • uv (Python package manager)
  • acme.sh (for SSL certificate management)
  • Cloudflare DNS (for DNS-01 ACME challenges)

Installation

  1. Clone the repository to /opt/haproxy/

  2. Install dependencies:

    cd /opt/haproxy/haproxy_mcp
    uv sync
    
  3. Start HAProxy:

    systemctl start haproxy
    
  4. Start the MCP server:

    systemctl start haproxy-mcp
    

Services

HAProxy (Podman Quadlet)

systemctl start haproxy      # Start
systemctl stop haproxy       # Stop
systemctl restart haproxy    # Restart
systemctl status haproxy     # Status
  • Quadlet config: /etc/containers/systemd/haproxy.container
  • Network mode: host

MCP Server (systemd)

systemctl start haproxy-mcp     # Start
systemctl stop haproxy-mcp      # Stop
systemctl restart haproxy-mcp   # Restart
journalctl -u haproxy-mcp -f    # Logs
  • Transport: streamable-http
  • Internal: http://localhost:8000/mcp
  • External: https://mcp.inouter.com/mcp (via HAProxy)

Environment Variables

Variable Default Description
MCP_HOST 0.0.0.0 MCP server bind host
MCP_PORT 8000 MCP server port
HAPROXY_HOST localhost HAProxy Runtime API host
HAPROXY_PORT 9999 HAProxy Runtime API port
HAPROXY_STATE_FILE /opt/haproxy/data/servers.state State file path
HAPROXY_MAP_FILE /opt/haproxy/conf/domains.map Map file path
HAPROXY_SERVERS_FILE /opt/haproxy/conf/servers.json Servers config path
HAPROXY_POOL_COUNT 100 Number of pool backends
HAPROXY_MAX_SLOTS 10 Max servers per pool
HAPROXY_CONTAINER haproxy Podman container name
LOG_LEVEL INFO Logging level (DEBUG/INFO/WARNING/ERROR)

Usage

Zero-Reload Domain Management

Adding a Domain

haproxy_add_domain("example.com", ip="10.0.0.1")
# Creates: pool_N_1 → 10.0.0.1:80

haproxy_add_domain("api.example.com", ip="10.0.0.1", http_port=8080)
# Creates: pool_N_1 → 10.0.0.1:8080

This will:

  1. Find an available pool (e.g., pool_5)
  2. Add to domains.map: example.com pool_5
  3. Update HAProxy runtime map via Runtime API
  4. Configure server in pool (HTTP only)
  5. Save to servers.json for persistence

No HAProxy reload required!

Pool Sharing

Multiple domains can share the same pool/backend servers:

# First domain gets its own pool
haproxy_add_domain("example.com", ip="10.0.0.1")
# Creates: example.com → pool_5

# Additional domains share the same pool
haproxy_add_domain("www.example.com", share_with="example.com")
haproxy_add_domain("api.example.com", share_with="example.com")
# Both use: pool_5 (same backend servers)

Benefits:

  • Saves pool slots (100 pools can serve unlimited domains)
  • Multiple domains share the same backend servers
  • Removing a shared domain keeps servers intact

Bulk Server Operations

haproxy_add_servers("api.example.com", '[
  {"slot": 1, "ip": "10.0.0.1", "http_port": 80},
  {"slot": 2, "ip": "10.0.0.2", "http_port": 80},
  {"slot": 3, "ip": "2001:db8::1", "http_port": 80}
]')

SSL/TLS Certificates

Certificates are managed via acme.sh with Cloudflare DNS validation and deployed to HAProxy with zero downtime using the Runtime API.

Item Value
ACME Client acme.sh (Google Trust Services CA)
Cert Directory /opt/haproxy/certs/
DNS Validation Cloudflare
# Issue a new certificate (with wildcard)
haproxy_issue_cert("example.com", wildcard=True)

# Renew a certificate
haproxy_renew_cert("example.com")

# List all certificates
haproxy_list_certs()

Health Checks

# System health
haproxy_health()
# Returns: {"status": "healthy", "components": {...}}

# Domain-specific health
haproxy_domain_health("api.example.com")
# Returns: {"status": "healthy", "servers": [...], "healthy_count": 1}

Status values: healthy (all UP), degraded (partial UP), down (all DOWN), no_servers

MCP Tools (29 total)

Domain Management

Tool Description
haproxy_list_domains List domains (use include_wildcards=true for wildcards)
haproxy_add_domain Add domain to pool (no reload), supports share_with for pool sharing
haproxy_remove_domain Remove domain from pool (no reload)

Server Management

Tool Description
haproxy_list_servers List servers for a domain
haproxy_add_server Add server to slot (1-10 or 0=auto), auto-saved
haproxy_add_servers Bulk add servers (JSON array), auto-saved
haproxy_remove_server Remove server from slot, auto-saved
haproxy_set_server_state Set state: ready/drain/maint
haproxy_set_server_weight Set weight (0-256)
haproxy_set_domain_state Set all servers of domain to ready/drain/maint
haproxy_wait_drain Wait for connections to drain (timeout configurable)

Health Check

Tool Description
haproxy_health System health (MCP, HAProxy, config files)
haproxy_domain_health Domain-specific health with server status
haproxy_get_server_health Get UP/DOWN/MAINT status for all servers

Monitoring

Tool Description
haproxy_stats HAProxy statistics
haproxy_backends List backends
haproxy_list_frontends List frontends with status
haproxy_get_connections Active connections per server

Configuration

Tool Description
haproxy_reload Validate, reload config, auto-restore servers
haproxy_check_config Validate config syntax
haproxy_save_state Save server state to disk
haproxy_restore_state Restore state from disk

Certificate Management (Zero-Downtime)

Tool Description
haproxy_list_certs List all certificates with expiry info
haproxy_cert_info Get detailed certificate info (expiry, issuer, SANs)
haproxy_issue_cert Issue new certificate via acme.sh + Cloudflare DNS
haproxy_renew_cert Renew specific certificate (force option available)
haproxy_renew_all_certs Renew all certificates due for renewal
haproxy_delete_cert Delete certificate from acme.sh and HAProxy
haproxy_load_cert Load/reload certificate into HAProxy (manual trigger)

MCP Connection

Tailscale (no auth):

claude mcp add --transport http haproxy http://100.108.39.107:8000/mcp

External (with auth):

curl -X POST https://mcp.inouter.com/mcp \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":0}'

Access rules:

Source Token Required
Tailscale (100.64.0.0/10) No
External Yes

Token location: /opt/haproxy/conf/mcp-token.env

Safety Features

  • Atomic file writes: Temp file + rename prevents corruption
  • File locking: Prevents race conditions on concurrent operations
  • Disk-first pattern: Config saved before HAProxy update, rollback on failure
  • Command validation: HAProxy responses checked for errors
  • Input validation: Domain format, IP (v4/v6), port range, slot limits
  • Bulk limits: Max 10 servers per bulk add, 10KB JSON size limit
  • Command batching: Multiple HAProxy commands sent in single TCP connection

Directory Structure

/opt/haproxy/
├── haproxy_mcp/         # MCP server package (streamable-http)
│   ├── server.py        # Main entry point
│   ├── config.py        # Configuration and constants
│   ├── exceptions.py    # Exception classes
│   ├── validation.py    # Input validation
│   ├── haproxy_client.py # HAProxy Runtime API client
│   ├── file_ops.py      # File I/O operations
│   ├── utils.py         # Parsing utilities
│   └── tools/           # MCP tools (29 total)
│       ├── domains.py   # Domain management (3 tools)
│       ├── servers.py   # Server management (7 tools)
│       ├── health.py    # Health checks (3 tools)
│       ├── monitoring.py # Monitoring (4 tools)
│       ├── configuration.py # Config management (4 tools)
│       └── certificates.py # Certificate management (7 tools)
├── conf/
│   ├── haproxy.cfg      # Main HAProxy config (100 pool backends)
│   ├── domains.map      # Domain → Pool mapping
│   ├── servers.json     # Server persistence (auto-managed)
│   ├── certificates.json # Certificate domain list (auto-managed)
│   └── mcp-token.env    # Bearer token for MCP auth
├── certs/               # SSL/TLS certificates (HAProxy PEM format)
├── data/                # Legacy state files
├── scripts/             # Utility scripts
└── run/                 # Runtime socket files

Startup Sequence

1. systemd starts haproxy.service
   ↓
2. HAProxy loads haproxy.cfg (100 pool backends)
   ↓
3. HAProxy loads domains.map (domain → pool mapping)
   ↓
4. systemd starts haproxy-mcp.service
   ↓
5. MCP server reads servers.json & certificates.json
   ↓
6. MCP server restores servers via Runtime API
   ↓
7. MCP server loads certificates via Runtime API (zero-downtime)
   ↓
8. Ready to serve traffic

HAProxy Runtime API

# Show domain mappings
echo "show map /usr/local/etc/haproxy/domains.map" | nc localhost 9999

# Add domain mapping
echo "add map /usr/local/etc/haproxy/domains.map example.com pool_5" | nc localhost 9999

# Show server state
echo "show servers state" | nc localhost 9999

# Set server address
echo "set server pool_1/pool_1_1 addr 10.0.0.1 port 80" | nc localhost 9999

# Enable server
echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999

License

MIT

Description
HAProxy Dynamic Load Balancer Management with MCP Interface
Readme MIT 302 KiB
Languages
Python 99.8%
Dockerfile 0.2%