adguard-mcp
by lidless-labs
README.md
<!-- content-guard: allow private-ipv4 file -->
<p align="center">
<img src="docs/assets/adguardctrl-social-preview.jpg" alt="adguardctrl banner" width="900">
</p>
<p align="center">
<a href="https://lidless.dev"><img src="docs/assets/marks/adguardctrl-circle.png" width="48" alt="Lidless Labs"></a>
</p>
<h1 align="center">adguardctrl</h1>
<p align="center">
<strong>An operator control CLI for AdGuard Home, with an MCP adapter for assistant workflows and back-compatible launchers.</strong>
</p>
<p align="center">
<a href="https://lidless.dev/adguard-mcp"><strong>Website</strong></a>
·
<a href="#quickstart"><strong>Quickstart</strong></a>
</p>
<p align="center">
<img src="https://shieldcn.dev/github/ci/lidless-labs/adguardctrl.svg?branch=main&workflow=ci.yml" alt="CI status">
<img src="https://shieldcn.dev/npm/@solomonneas/adguard-mcp.svg" alt="npm version">
<img src="https://shieldcn.dev/badge/license-MIT-green.svg" alt="MIT license">
</p>
`adguardctrl` is an operator control CLI for AdGuard Home, the self-hosted DNS sinkhole. It gives shell, cron, CI, and assistant workflows one typed control surface for inspecting network-wide DNS filtering across one or more boxes. The npm package remains `@solomonneas/adguard-mcp` for compatibility and still ships the back-compatible `adguard-mcp` bin; new MCP launchers can start the adapter with `adguardctrl mcp`.
## What it does
`adguardctrl` speaks the AdGuard Home API across one or more instances, plus optional AdGuardHome Sync status and control. The direct CLI is read-only today, covering server status, stats, the DNS query log, filter lists, named clients, DNS rewrites, DNS and TLS config, DHCP status, and a `check-host` lookup that shows exactly what AdGuard would do with a hostname.
For assistant clients, `adguardctrl mcp` starts the stdio MCP adapter used by Claude Desktop, Claude Code, Codex CLI, OpenClaw, Hermes, and other Model Context Protocol clients. That adapter keeps the original `adguard-mcp` tool surface and safety model: reads are open, writes require an explicit `confirm: true`, and destructive operations additionally require `destructive: true`, so an agent cannot disable filtering or wipe rules on a hallucinated call.
## Quickstart
Install globally:
```
npm install -g @solomonneas/adguard-mcp
```
Or run via npx with no install:
```
npx -y @solomonneas/adguard-mcp
```
Then use `adguardctrl` from your shell, or wire the package into an MCP client. The minimal config for any client that speaks the standard `mcpServers` shape (Claude Desktop, Claude Code):
```json
{
"mcpServers": {
"adguard": {
"command": "npx",
"args": ["-y", "@solomonneas/adguard-mcp"],
"env": {
"ADGUARD_PRIMARY_URL": "http://192.0.2.10",
"ADGUARD_PRIMARY_USERNAME": "admin",
"ADGUARD_PRIMARY_PASSWORD": "your-password"
}
}
}
}
```
Once connected, ask your client to call `adguard_status` to confirm it can reach the box. Reads work immediately; writes need the `confirm: true` flag and destructive ops also need `destructive: true`.
## Tools
**Reads (14):** `adguard_status`, `adguard_stats`, `adguard_query_log`, `adguard_list_filter_lists`, `adguard_list_user_rules`, `adguard_list_clients`, `adguard_list_blocked_services_catalog`, `adguard_check_host`, `adguard_get_blocked_services`, `adguard_get_dns_config`, `adguard_get_safesearch_settings`, `adguard_sync_status`, `adguard_sync_health`, `adguard_sync_logs`.
| Tool | Description |
|---|---|
| `adguard_status` | Server status + protection state (`GET /control/status`). |
| `adguard_stats` | Stats window: top queries, blocked counts, clients (`GET /control/stats`). |
| `adguard_query_log` | DNS query log slice with filters (`GET /control/querylog`). |
| `adguard_list_filter_lists` | Subscribed blocklists + allowlists (`GET /control/filtering/status`). |
| `adguard_list_user_rules` | Custom user rules (`GET /control/filtering/status`). |
| `adguard_list_clients` | Configured named clients (`GET /control/clients`). |
| `adguard_list_blocked_services_catalog` | Available service IDs to block (`GET /control/blocked_services/services`). |
| `adguard_check_host` | Test what AGH would do with a hostname: filter decision, matched rules, CNAME chain, IPs (`GET /control/filtering/check_host`). |
| `adguard_get_blocked_services` | Global blocked-services list + weekly schedule (`GET /control/blocked_services/get`). |
| `adguard_get_dns_config` | DNS upstreams, bootstrap, cache, parallel resolution, blocking mode (`GET /control/dns_info`). |
| `adguard_get_safesearch_settings` | SafeSearch enabled state + per-engine flags (`GET /control/safesearch/status`). |
| `adguard_sync_status` | AdGuardHome Sync origin/replica status (`GET /api/v1/status`). |
| `adguard_sync_health` | AdGuardHome Sync health check (`HEAD /healthz`). |
| `adguard_sync_logs` | AdGuardHome Sync in-memory logs (`GET /api/v1/logs`). |
**Safe writes (13, require `confirm: true`):** `adguard_add_user_rule`, `adguard_remove_user_rule`, `adguard_add_filter_list`, `adguard_remove_filter_list`, `adguard_toggle_filter_list`, `adguard_set_client_blocked_services`, `adguard_refresh_filter_lists`, `adguard_add_client`, `adguard_update_client`, `adguard_set_blocked_services`, `adguard_toggle_safesearch`, `adguard_toggle_safebrowsing`, `adguard_sync_run`.
| Tool | Description |
|---|---|
| `adguard_add_user_rule` | Append a single user filter rule (`POST /control/filtering/set_rules`). |
| `adguard_remove_user_rule` | Remove a single user filter rule by exact match (`POST /control/filtering/set_rules`). |
| `adguard_add_filter_list` | Subscribe to a new blocklist or allowlist URL (`POST /control/filtering/add_url`). |
| `adguard_remove_filter_list` | Unsubscribe from a filter list by URL (`POST /control/filtering/remove_url`). |
| `adguard_toggle_filter_list` | Enable or disable a subscribed filter list (`POST /control/filtering/set_url`). |
| `adguard_set_client_blocked_services` | Set per-client blocked services + schedule (`POST /control/clients/update`). |
| `adguard_refresh_filter_lists` | Force refresh subscribed filter lists immediately (`POST /control/filtering/refresh`). |
| `adguard_add_client` | Register a new named client with per-client settings (`POST /control/clients/add`). |
| `adguard_update_client` | Full update for an existing named client; body is nested `{name, data}` (`POST /control/clients/update`). |
| `adguard_set_blocked_services` | Set GLOBAL blocked services + optional weekly schedule; accepts HH:MM strings or ms (`PUT /control/blocked_services/update`). |
| `adguard_toggle_safesearch` | Enable or disable SafeSearch globally with per-engine flags (`PUT /control/safesearch/settings`). |
| `adguard_toggle_safebrowsing` | Enable or disable AGH SafeBrowsing (`POST /control/safebrowsing/enable` or `/disable`). |
| `adguard_sync_run` | Trigger AdGuardHome Sync immediately (`POST /api/v1/sync`). |
**Destructive (6, require `confirm: true` + `destructive: true`):** `adguard_replace_user_rules`, `adguard_toggle_protection`, `adguard_delete_client`, `adguard_clear_query_log`, `adguard_reset_stats`, `adguard_sync_clear_logs`.
| Tool | Description |
|---|---|
| `adguard_replace_user_rules` | Wholesale replace the user rules block (`POST /control/filtering/set_rules`). |
| `adguard_toggle_protection` | Enable or disable global filtering; off stops ALL blocking (`POST /control/protection`). |
| `adguard_delete_client` | Remove a configured named client; per-client rules and stats are lost (`POST /control/clients/delete`). |
| `adguard_clear_query_log` | Wipe the DNS query log (`POST /control/querylog_clear`). |
| `adguard_reset_stats` | Zero the stats window (`POST /control/stats_reset`). |
| `adguard_sync_clear_logs` | Clear AdGuardHome Sync in-memory logs (`POST /api/v1/clear-logs`). |
## Configuration
Set per-instance env vars. At least one instance is required.
```
ADGUARD_PRIMARY_URL=http://192.0.2.10
ADGUARD_PRIMARY_USERNAME=admin
ADGUARD_PRIMARY_PASSWORD=<password>
# Optional second instance:
ADGUARD_SECONDARY_URL=http://192.0.2.11
ADGUARD_SECONDARY_USERNAME=admin
ADGUARD_SECONDARY_PASSWORD=<password>
# Optional: which instance is default when a tool omits the `instance` arg:
ADGUARD_DEFAULT_INSTANCE=primary
# Optional: per-request HTTP timeout in milliseconds (default 30000; minimum 1000):
ADGUARD_REQUEST_TIMEOUT_MS=30000
```
Instance names are derived from the env-var middle segment (case-insensitive). Add `ADGUARD_LIVINGROOM_URL/USERNAME/PASSWORD` and the MCP picks it up on next start.
Every tool accepts optional `instance: "<name>"` to address a non-default box.
AdGuardHome Sync is optional and uses a separate env prefix so it does not collide with AdGuard Home instance names:
```
ADGUARDHOME_SYNC_URL=http://192.0.2.10:8080
# Optional, only when the Sync API is configured with Basic auth:
ADGUARDHOME_SYNC_USERNAME=sync
ADGUARDHOME_SYNC_PASSWORD=<password>
```
`ADGUARD_SYNC_URL/USERNAME/PASSWORD` is also accepted as an alias and is reserved for the Sync server, not an AdGuard Home instance named `sync`.
If neither Sync URL env var is set, Sync tools remain listed but return a clear config error when called.
## CLI
`adguardctrl` is the package's primary operator entry point for shells, cron, and CI. It shares the `AdGuardClient` / `AdGuardSyncClient` core with the MCP adapter and reads the same env config. It exposes only the Tier-1 read tools; writes stay in the MCP/plugin surface behind the tier gates.
```bash
npx @solomonneas/adguard-mcp@latest status
# or, installed globally:
adguardctrl status
adguardctrl stats
adguardctrl querylog --limit 20 --blocked-only
adguardctrl check-host youtube.com --client kid-tablet
adguardctrl clients list
adguardctrl filters list
adguardctrl rewrites list
adguardctrl dns-config
adguardctrl tls status # private key is redacted
adguardctrl sync health # exit 1 if Sync is not healthy (cron-friendly)
adguardctrl status --json # raw JSON for piping
adguardctrl status --instance secondary
```
Run `adguardctrl help` for the full command and flag list. `--instance <name>` targets a non-default AdGuard Home box; `--json` emits raw JSON instead of the concise human-readable summary. Exit codes: `0` success, `1` runtime error (backend unreachable / call failed, and `sync health` when not healthy), `2` usage error (unknown command/flag or bad value).
### Starting the MCP server
`adguardctrl mcp` (or the back-compat `adguard-mcp` bin) starts the stdio MCP adapter. If a launcher referenced the file path `dist/mcp-server.js` directly, it keeps working; new launchers can point at `dist/mcp-bin.js` (or `dist/cli.js mcp`). Launchers that use the `adguard-mcp` bin name need no change.
## Setup per client
### Claude Desktop
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): use the `mcpServers` block from [Quickstart](#quickstart).
### Claude Code
```bash
claude mcp add adguard -s user -- npx -y @solomonneas/adguard-mcp
```
Then export env vars in your shell (`~/.bashrc`, `~/.zshrc`) or pass `--env` flags.
### OpenClaw
Plugin loads automatically once installed. Config goes in your `~/.openclaw/openclaw.json` `plugins.entries.adguard` (or use the bundled `openclaw.plugin.json`):
```json
{
"plugins": {
"entries": {
"adguard": {
"package": "@solomonneas/adguard-mcp",
"activation": { "onStartup": true }
}
}
}
}
```
Env vars from `~/.openclaw/workspace/.env` are inherited by the plugin.
### Hermes Agent
Add to `~/.config/hermes/agents.yaml`:
```yaml
mcp_servers:
adguard:
command: npx
args: ["-y", "@solomonneas/adguard-mcp"]
env:
ADGUARD_PRIMARY_URL: http://192.0.2.10
ADGUARD_PRIMARY_USERNAME: admin
ADGUARD_PRIMARY_PASSWORD: your-password
```
### Codex CLI
`~/.codex/config.toml`:
```toml
[mcp_servers.adguard]
command = "npx"
args = ["-y", "@solomonneas/adguard-mcp"]
[mcp_servers.adguard.env]
ADGUARD_PRIMARY_URL = "http://192.0.2.10"
ADGUARD_PRIMARY_USERNAME = "admin"
ADGUARD_PRIMARY_PASSWORD = "your-password"
```
## Safety
- Credentials only live in memory after env-load and are redacted from logs and error messages.
- Tier 2 writes require an explicit `confirm: true` arg; the JSON schema documents this on every write tool.
- Tier 3 destructive ops additionally require `destructive: true`. The model cannot disable protection or overwrite the rules block from a hallucinated tool call.
## Why not just give the agent the AdGuard Home API?
- **The raw AdGuard Home API has no agent-safety layer.** Every endpoint is one call away, including the ones that disable all blocking or wipe your rules. The MCP adapter keeps reads open and gates writes behind `confirm: true` and destructive ops behind `destructive: true`, so a hallucinated tool call cannot silently break your network.
- **`curl` or a generic HTTP MCP server** would mean the model hand-builds request bodies, handles Basic auth, and remembers which AdGuard quirk applies (the nested `{name, data}` client body, milliseconds-from-midnight schedules, the `PUT` vs `POST` split). `adguardctrl` and its MCP adapter encode those as typed commands and tools, so the caller picks an intent, not an HTTP shape.
- **A single-instance integration** does not match a real homelab. `adguardctrl` resolves any number of instances from env vars by name and lets commands or MCP tools target a non-default box with one `instance` arg, plus optional AdGuardHome Sync control alongside.
- **Clicking the web dashboard** works, but not from inside an assistant and not across several boxes at once. This is the same control surface, available to the agent you already have open.
## What adguardctrl is not
`adguardctrl` is not a hosted service, a replacement for the AdGuard Home dashboard, or an autonomous network manager.
It does not:
- run a daemon, scheduler, or background process
- store or proxy your DNS traffic
- make any write without an explicit `confirm: true` flag from the caller
- perform a destructive operation without `confirm: true` and `destructive: true` together
- talk to AdGuard's hosted DNS product; it targets self-hosted AdGuard Home instances you run
## License
MIT. See [LICENSE](LICENSE).
---
<p align="center"><a href="https://lidless.dev">Part of <strong>Lidless Labs</strong></a> · the eye does not close</p>
<p align="center"><sub><strong>Homelab:</strong> <a href="https://github.com/lidless-labs/proxmox-mcp">proxmox-mcp</a> · <a href="https://github.com/lidless-labs/immichctrl">immichctrl</a> · <a href="https://github.com/lidless-labs/jellyctrl">jellyctrl</a> · <a href="https://github.com/lidless-labs/proxguard">proxguard</a></sub></p>
<p align="center"><sub><a href="https://lidless.dev">All tools</a> · <a href="https://github.com/lidless-labs">Lidless Labs on GitHub</a></sub></p>
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessResponsive