{
  "markdown": "# SWAG MCP\n\n<!-- mcp-name: tv.tootie/swag-mcp -->\n\n[![PyPI](https://img.shields.io/pypi/v/swag-mcp)](https://pypi.org/project/swag-mcp/) [![ghcr.io](https://img.shields.io/badge/ghcr.io-jmagar%2Fswag--mcp-blue?logo=docker)](https://github.com/jmagar/swag-mcp/pkgs/container/swag-mcp)\n\nMCP server for managing SWAG reverse-proxy configuration files, backups, logs, and health checks. Uses a single `swag` action router backed by local filesystem or SSH-accessible SWAG config storage.\n\n## Overview\n\nSWAG MCP generates and manages nginx subdomain proxy configurations for [SWAG (Secure Web Application Gateway)](https://github.com/linuxserver/docker-swag). Every generated config includes MCP-compatible security headers unconditionally, making configs suitable for both standard web services and MCP/AI services.\n\n## What this repository ships\n\n- `swag_mcp/`: server, config, middleware, models, services, tools, and templates\n- `config/`: local config and test assets\n- `docs/`: template notes, test commands, and design records\n- `.claude-plugin/`, `.codex-plugin/`, `gemini-extension.json`: client manifests\n- `docker-compose.yaml`, `Dockerfile`, `entrypoint.sh`: container deployment\n\n## MCP surface\n\n### Tool\n\n| Tool | Purpose |\n| --- | --- |\n| `swag` | Unified action router for config, logs, backups, and health checks |\n| `swag_help` | Return help for the SWAG MCP server — lists all available actions and sub-actions |\n\n### Actions\n\n| Action | Purpose | Required params |\n| --- | --- | --- |\n| `list` | List config files | none |\n| `create` | Create a new reverse-proxy config | `config_name`, `server_name`, `upstream_app`, `upstream_port` |\n| `view` | View config contents | `config_name` |\n| `edit` | Replace config contents | `config_name`, `new_content` |\n| `update` | Update a specific field | `config_name`, `update_field`, `update_value` |\n| `remove` | Remove a config | `config_name` |\n| `logs` | Read SWAG logs | none |\n| `backups` | List or clean up backup files | `backup_action` |\n| `health_check` | Probe a service endpoint | `domain` |\n\n### Resources\n\n| URI | Type | Description |\n| --- | --- | --- |\n| `swag://` | Directory | All active `.conf` files (excluding `.sample`) |\n| `swag://configs/live` | Stream | Real-time config change events |\n| `swag://health/stream` | Stream | Real-time health status for monitored services |\n| `swag://logs/stream` | Stream | Live nginx error log tail |\n\n## Installation\n\n### Plugin (recommended)\n\nInstall as a Claude Code plugin. You will be prompted for:\n\n- **SWAG Proxy Configs Path** -- local path to proxy-confs directory\n- **SWAG MCP Server URL** -- base URL of the running HTTP server\n- **SWAG MCP API Token** -- bearer token for the HTTP server\n\nThe plugin connects to the server's native streamable-HTTP endpoint.\n\n```bash\n/plugin marketplace add jmagar/claude-homelab\n/plugin install swag-mcp @jmagar-claude-homelab\n```\n\nThe plugin uses native HTTP transport and appends `/mcp` to the configured server URL.\n\n### Docker Compose\n\n```bash\ncp .env.example .env\nchmod 600 .env\n# Edit .env with your paths and token\ndocker compose up -d\n```\n\nThe container always runs on internal port 8000. Compose publishes it on\n`127.0.0.1:49152` by default; set `SWAG_MCP_PORT` to control the host port and\n`SWAG_MCP_BIND_ADDRESS` only when you intentionally expose it beyond loopback.\n\n### Local development\n\n```bash\njust setup       # copies .env.example and runs uv sync\njust dev         # starts the server\n```\n\n## Configuration\n\nTwo deployment paths are supported:\n\n| Path | Transport | Credentials | Auth |\n|------|-----------|-------------|------|\n| **Plugin (HTTP)** | http | `userConfig` in plugin settings | Bearer token |\n| **Docker (HTTP)** | http | `.env` file | Bearer token |\n\nSee [docs/CONFIG.md](docs/CONFIG.md) for full variable reference. All variables use the `SWAG_MCP_` prefix.\n\n### Core\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_PROXY_CONFS_PATH` | no | `/swag/nginx/proxy-confs` | Local path to SWAG proxy confs directory |\n| `SWAG_MCP_PROXY_CONFS_URI` | no | `` | Overrides `PROXY_CONFS_PATH` when set. Accepts a local path or SSH URI. |\n| `SWAG_MCP_SWAG_LOG_BASE_PATH` | no | `/swag/log` | Base path for SWAG log files (local or remote) |\n| `SWAG_MCP_TEMPLATE_PATH` | no | `templates` | Path to Jinja2 templates directory |\n\n### Server\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_HOST` | no | `127.0.0.1` | Bind address for the MCP server |\n| `SWAG_MCP_BIND_ADDRESS` | no | `127.0.0.1` | Docker host bind address for the published MCP port |\n| `SWAG_MCP_PORT` | no | `49152` | Host-side port (Docker only; container always uses 8000) |\n| `SWAG_MCP_TOKEN` | recommended | `` | Bearer token enforced by FastMCP for direct MCP server access |\n| `SWAG_MCP_NO_AUTH` | no | `false` | Set `true` only for loopback/proxy-isolated deployments without server auth |\n\n### Defaults\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_DEFAULT_WEB_AUTH_METHOD` | no | `authelia` | Web endpoint auth method used when `create` omits `auth_method`; this is not MCP server auth |\n| `SWAG_MCP_DEFAULT_QUIC_ENABLED` | no | `false` | QUIC default for new configs |\n\n### Backups\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_BACKUP_RETENTION_DAYS` | no | `30` | Days to keep backup files. Cleanup runs at startup and on `backups action=cleanup`. |\n\n### Logging\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_LOG_LEVEL` | no | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL` |\n| `SWAG_MCP_LOG_DIRECTORY` | no | `/app/.swag-mcp/logs` | Directory for rotating log files |\n| `SWAG_MCP_LOG_FILE_ENABLED` | no | `true` | Write logs to files in addition to stdout |\n| `SWAG_MCP_LOG_FILE_MAX_BYTES` | no | `10485760` | Max log file size before rotation (10 MB) |\n| `SWAG_MCP_ENABLE_STRUCTURED_LOGGING` | no | `false` | Emit JSON structured logs |\n| `SWAG_MCP_LOG_PAYLOADS` | no | `false` | Include request/response bodies in logs |\n| `SWAG_MCP_LOG_PAYLOAD_MAX_LENGTH` | no | `1000` | Max payload length when `LOG_PAYLOADS=true` |\n\n### Performance\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_SLOW_OPERATION_THRESHOLD_MS` | no | `1000` | Log a warning when any operation exceeds this threshold |\n| `SWAG_MCP_ENABLE_RETRY_MIDDLEWARE` | no | `true` | Automatically retry failed operations |\n| `SWAG_MCP_MAX_RETRIES` | no | `3` | Max retry attempts |\n\n### Health checks\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_HEALTH_CHECK_INSECURE` | no | `false` | Skip SSL verification for health probes. Not recommended for production. |\n\n### Rate limiting\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `SWAG_MCP_RATE_LIMIT_ENABLED` | no | `false` | Enable the rate-limiting middleware |\n| `SWAG_MCP_RATE_LIMIT_RPS` | no | `10.0` | Sustained requests per second |\n| `SWAG_MCP_RATE_LIMIT_BURST` | no | `20` | Burst capacity above the RPS limit |\n\n## Authentication methods\n\nPass `auth_method` to `create` to control which SWAG auth snippet the config includes. The default is set by `SWAG_MCP_DEFAULT_WEB_AUTH_METHOD`.\n\n| Method | When to use |\n| --- | --- |\n| `none` | Public services, or services behind an external SSO not managed by SWAG |\n| `basic` | Simple HTTP basic auth. Credentials managed by SWAG. |\n| `ldap` | LDAP/Active Directory authentication via SWAG's built-in LDAP support |\n| `authelia` | Self-hosted SSO with 2FA. Most common for homelab services. |\n| `authentik` | Self-hosted identity provider with rich policy engine |\n| `tinyauth` | Lightweight single-user auth for internal tools |\n| `oauth` | OAuth 2.1 gateway (`mcp-oauth`). Use for MCP/AI services that handle their own auth flow. |\n\n`none` exposes the service without authentication. Only use it if access is restricted at the network or proxy layer.\n\n## Template system\n\nAll configs are generated from a single Jinja2 template: `templates/mcp.subdomain.conf.j2`. Only subdomain-style configs are supported (`service.domain.tld`).\n\n### Template variables\n\n| Variable | Type | Required | Description |\n| --- | --- | --- | --- |\n| `service_name` | string | yes | Derived from `config_name` (e.g., `jellyfin`) |\n| `server_name` | string | yes | Domain name (e.g., `media.example.com`) |\n| `upstream_app` | string | yes | Container name or IP for the main service |\n| `upstream_port` | integer | yes | Port the main service listens on |\n| `upstream_proto` | string | yes | `http` or `https` |\n| `mcp_upstream_app` | string | no | Container name or IP for the MCP endpoint. Defaults to `upstream_app`. |\n| `mcp_upstream_port` | integer | no | Port for the MCP endpoint. Defaults to `upstream_port`. |\n| `mcp_upstream_proto` | string | no | Protocol for the MCP endpoint. Defaults to `upstream_proto`. |\n| `auth_method` | string | yes | One of the auth methods listed above |\n| `enable_quic` | boolean | yes | Add `listen 443 quic` listeners and `Alt-Svc` header |\n\n### MCP security headers\n\nThese headers are included unconditionally in every generated config, regardless of auth method:\n\n| Header | Value | Purpose |\n| --- | --- | --- |\n| `X-MCP-Version` | `2025-11-25` | Advertises the MCP protocol version |\n| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limits referrer leakage |\n\nDNS rebinding protection is also applied unconditionally. The `origin_valid` variable is set to `1` only for empty origins, `https://$server_name`, localhost variants, and `*.anthropic.com` / `*.claude.ai` origins. Requests to `/mcp` with an invalid origin receive a `403` JSON error.\n\n## Split routing\n\nSplit routing sends the main application traffic to one upstream and MCP traffic to a different upstream. Use it when your MCP/AI service runs on a separate container or host (e.g., a GPU server).\n\nWithout split routing, both `/` and `/mcp` proxy to the same `upstream_app:upstream_port`.\n\nWith split routing:\n- `/` and `/health` → `upstream_app:upstream_port`\n- `/mcp` and `/session` → `mcp_upstream_app:mcp_upstream_port`\n\n### Example: Jellyfin with a separate AI backend\n\n```\naction=create\nconfig_name=jellyfin.subdomain.conf\nserver_name=jellyfin.example.com\nupstream_app=jellyfin\nupstream_port=8096\nupstream_proto=http\nmcp_upstream_app=ai-gpu-server\nmcp_upstream_port=8080\nmcp_upstream_proto=http\nauth_method=authelia\n```\n\nThis generates:\n- `location /` → `http://jellyfin:8096` (protected by Authelia)\n- `location /mcp` → `http://ai-gpu-server:8080` (protected by OAuth verify)\n- `location /health` → `http://jellyfin:8096` (no auth, for health probes)\n\nIf `mcp_upstream_app` is omitted, all three locations proxy to `jellyfin:8096`.\n\n## SSH / remote configuration\n\nSet `SWAG_MCP_PROXY_CONFS_URI` to manage configs on a remote SWAG host over SSH.\n\nSupported URI formats:\n\n```\n# Default key, default port 22\nSWAG_MCP_PROXY_CONFS_URI=swag-server:/mnt/appdata/swag/nginx/proxy-confs\n\n# Explicit user\nSWAG_MCP_PROXY_CONFS_URI=admin@swag-server:/mnt/appdata/swag/nginx/proxy-confs\n\n# Explicit user and port\nSWAG_MCP_PROXY_CONFS_URI=admin@swag-server:2222:/mnt/appdata/swag/nginx/proxy-confs\n```\n\nRequirements:\n- SSH key-based (passwordless) access to the remote host\n- Mount an app-specific SSH directory with only the needed key, config, and `known_hosts` entries into the container (see `SWAG_MCP_SSH_HOST_PATH` in `.env.example`)\n- `SWAG_MCP_SWAG_LOG_BASE_PATH` should point to the log base path on the same remote host\n\nWhen `SWAG_MCP_PROXY_CONFS_URI` is set it takes precedence over `SWAG_MCP_PROXY_CONFS_PATH`.\n\n## Usage examples\n\n### List\n\n```\nswag action=list\nswag action=list list_filter=active\nswag action=list list_filter=samples query=plex offset=0 limit=20\n```\n\n`list_filter` accepts `all` (default), `active` (`.conf` files only), or `samples` (`.conf.sample` files only).\n\n### Create\n\n```\nswag action=create\n     config_name=jellyfin.subdomain.conf\n     server_name=jellyfin.example.com\n     upstream_app=jellyfin\n     upstream_port=8096\n```\n\nAfter creation, a health check runs automatically against `server_name`.\n\n### View\n\n```\nswag action=view config_name=jellyfin.subdomain.conf\n```\n\n### Edit\n\nReplaces the full file content:\n\n```\nswag action=edit\n     config_name=jellyfin.subdomain.conf\n     new_content=\"...\"\n     create_backup=true\n```\n\n### Update\n\nUpdates a single field in place:\n\n```\nswag action=update config_name=jellyfin.subdomain.conf update_field=port update_value=8097\nswag action=update config_name=jellyfin.subdomain.conf update_field=upstream update_value=jellyfin-new\nswag action=update config_name=jellyfin.subdomain.conf update_field=app update_value=jellyfin:8097\nswag action=update config_name=jellyfin.subdomain.conf update_field=add_mcp update_value=/mcp\n```\n\n`update_field` options:\n\n| Value | `update_value` format | Effect |\n| --- | --- | --- |\n| `port` | port number | Updates `upstream_port` |\n| `upstream` | container name | Updates `upstream_app` |\n| `app` | `app:port` | Updates both `upstream_app` and `upstream_port` |\n| `add_mcp` | path (e.g., `/mcp`) | Injects an MCP location block into the config |\n\nAfter an update, a health check runs automatically.\n\n### Remove\n\n```\nswag action=remove config_name=jellyfin.subdomain.conf\nswag action=remove config_name=jellyfin.subdomain.conf create_backup=false\n```\n\n### Logs\n\n```\nswag action=logs log_type=nginx-error lines=100\nswag action=logs log_type=nginx-access lines=50\nswag action=logs log_type=fail2ban lines=200\n```\n\n#### Log types and format\n\n| `log_type` | Source file | Format |\n| --- | --- | --- |\n| `nginx-access` | `$SWAG_LOG_BASE_PATH/nginx/access.log` | Combined nginx access log: IP, timestamp, method, path, status, bytes, referrer, user-agent |\n| `nginx-error` | `$SWAG_LOG_BASE_PATH/nginx/error.log` | Nginx error format: timestamp, level, PID, message |\n| `fail2ban` | `$SWAG_LOG_BASE_PATH/fail2ban/fail2ban.log` | fail2ban log: timestamp, level, jail name, action, IP |\n| `letsencrypt` | `$SWAG_LOG_BASE_PATH/letsencrypt/letsencrypt.log` | Certbot output: certificate request, renewal, errors |\n| `renewal` | `$SWAG_LOG_BASE_PATH/letsencrypt/renewal.log` | Periodic renewal cron output |\n\n`lines` accepts 1–1000 (default 50).\n\n### Backups\n\n```\nswag action=backups backup_action=list\nswag action=backups backup_action=cleanup retention_days=7\nswag action=backups backup_action=cleanup retention_days=0\n```\n\n`backup_action=cleanup` removes backup files older than `retention_days`. Pass `retention_days=0` to use the server default (`SWAG_MCP_BACKUP_RETENTION_DAYS`).\n\nBackup files are named `<original>.backup.<timestamp>`. They are created automatically before any `edit`, `update`, or `remove` operation when `create_backup=true` (the default).\n\nCleanup also runs at server startup. The startup cleanup uses `SWAG_MCP_BACKUP_RETENTION_DAYS`.\n\n### Health check\n\n```\nswag action=health_check domain=jellyfin.example.com\nswag action=health_check domain=jellyfin.example.com timeout=10 follow_redirects=false\n```\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `success` | boolean | `true` only for 2xx responses, plus 406 on `/mcp` because that confirms the MCP endpoint exists but requires POST. Other statuses fail and are reported in `endpoint_results`. |\n| `domain` | string | Domain that was checked |\n| `status_code` | integer or null | HTTP status code returned |\n| `response_time_ms` | integer or null | Round-trip time in milliseconds |\n| `error` | string or null | Error message if the probe failed |\n| `endpoint_results` | array | Per-endpoint probe attempts with URL, success flag, status, timing, and error detail |\n\n`timeout` accepts 1–300 seconds (default 30). The server adds a 10-second buffer on top of `timeout` for its own wait.\n\nSSL certificate verification can be disabled globally with `SWAG_MCP_HEALTH_CHECK_INSECURE=true`.\n\n## Development commands\n\n```bash\njust setup        # copy .env.example and install dependencies\njust dev          # start the server with uv\njust lint         # ruff check\njust fmt          # ruff format\njust typecheck    # ty check\njust test         # pytest\njust build        # build a local swag-mcp image for manual testing\njust up           # docker compose up -d\njust down         # docker compose down\njust logs         # docker compose logs -f\njust health       # curl http://127.0.0.1:${SWAG_MCP_PORT:-49152}/health | jq\njust gen-token    # generate a random bearer token\n```\n\n## Operations\n\nThe Docker service publishes `127.0.0.1:49152` by default. Keep production MCP\nports on high-numbered, documented ports and run `just preflight` before\ndeployment to validate the external Docker network and selected host port.\n\nOperational failure signals include Docker health becoming unhealthy, repeated\ntool error responses, timeout messages for create/edit/update/log operations,\nslow-operation warnings, health-check failures for known-good domains, and\nnginx error log spikes. Use [docs/mcp/LOGS.md](docs/mcp/LOGS.md) for signal\ntriage and [docs/mcp/DEPLOY.md](docs/mcp/DEPLOY.md) for rollback and restore.\n\nBackups are created before destructive edits, updates, and removals. To restore,\nlist backups with `swag(action=\"backups\", backup_action=\"list\")`, copy the\nchosen backup content back with `swag(action=\"edit\", create_backup=true)`,\nvalidate nginx syntax, reload SWAG, and record the restore in\n`docs/deployment-log.md`.\n\n## Verification\n\n```bash\njust lint\njust typecheck\njust test\n```\n\nCheck the server health endpoint:\n\n```bash\njust health\n# {\"status\": \"healthy\", \"service\": \"swag-mcp\", \"version\": \"1.1.6\"}\n```\n\nThe `/health` endpoint is also used by the Docker `HEALTHCHECK` directive. It always returns:\n\n| Field | Value |\n| --- | --- |\n| `status` | `\"healthy\"` |\n| `service` | `\"swag-mcp\"` |\n| `version` | Package version from `importlib.metadata` |\n\n## Related plugins\n\n| Plugin | Category | Description |\n|--------|----------|-------------|\n| [homelab-core](https://github.com/jmagar/claude-homelab) | core | Core agents, commands, skills, and setup/health workflows for homelab management. |\n| [overseerr-mcp](https://github.com/jmagar/overseerr-mcp) | media | Search movies and TV shows, submit requests, and monitor failed requests via Overseerr. |\n| [unraid-mcp](https://github.com/jmagar/unraid-mcp) | infrastructure | Query, monitor, and manage Unraid servers: Docker, VMs, array, parity, and live telemetry. |\n| [unifi-mcp](https://github.com/jmagar/unifi-mcp) | infrastructure | Monitor and manage UniFi devices, clients, firewall rules, and network health. |\n| [gotify-mcp](https://github.com/jmagar/gotify-mcp) | utilities | Send and manage push notifications via a self-hosted Gotify server. |\n| [synapse-mcp](https://github.com/jmagar/synapse-mcp) | infrastructure | Docker management (Flux) and SSH remote operations (Scout) across homelab hosts. |\n| [arcane-mcp](https://github.com/jmagar/arcane-mcp) | infrastructure | Manage Docker environments, containers, images, volumes, networks, and GitOps via Arcane. |\n| [syslog-mcp](https://github.com/jmagar/syslog-mcp) | infrastructure | Receive, index, and search syslog streams from all homelab hosts via SQLite FTS5. |\n| [plugin-lab](https://github.com/jmagar/plugin-lab) | dev-tools | Scaffold, review, align, and deploy homelab MCP plugins with agents and canonical templates. |\n\n## License\n\nMIT\n",
  "bytes": 19417,
  "sha": "5679f5cc91ea26b7ad3f874c7b13a81b00f27f0c4b11b0e00e4c4f12b3f92b98",
  "repo_slug": "jmagar/swag-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_jmagar_swag_mcp_66cb005c/readme"
}