Some checks failed
CI/CD / build-and-deploy (push) Failing after 7s
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.com→pool_N - 100 pools available:
pool_1topool_100 - Each pool has 10 server slots (
pool_N_1topool_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
-
Clone the repository to
/opt/haproxy/ -
Install dependencies:
cd /opt/haproxy/haproxy_mcp uv sync -
Start HAProxy:
systemctl start haproxy -
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:
- Find an available pool (e.g.,
pool_5) - Add to
domains.map:example.com pool_5 - Update HAProxy runtime map via Runtime API
- Configure server in pool (HTTP only)
- Save to
servers.jsonfor 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
Languages
Python
99.8%
Dockerfile
0.2%