@samik081/mcp-adguard-home 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/core/auth.d.ts +8 -0
- package/dist/core/auth.js +11 -0
- package/dist/core/client.d.ts +36 -0
- package/dist/core/client.js +155 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +69 -0
- package/dist/core/errors.d.ts +28 -0
- package/dist/core/errors.js +53 -0
- package/dist/core/logger.d.ts +10 -0
- package/dist/core/logger.js +21 -0
- package/dist/core/server.d.ts +15 -0
- package/dist/core/server.js +33 -0
- package/dist/core/tools.d.ts +12 -0
- package/dist/core/tools.js +54 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +44 -0
- package/dist/tools/access.d.ts +7 -0
- package/dist/tools/access.js +67 -0
- package/dist/tools/blocked_services.d.ts +9 -0
- package/dist/tools/blocked_services.js +156 -0
- package/dist/tools/clients.d.ts +7 -0
- package/dist/tools/clients.js +217 -0
- package/dist/tools/dhcp.d.ts +7 -0
- package/dist/tools/dhcp.js +294 -0
- package/dist/tools/dns.d.ts +7 -0
- package/dist/tools/dns.js +131 -0
- package/dist/tools/filtering.d.ts +7 -0
- package/dist/tools/filtering.js +224 -0
- package/dist/tools/global.d.ts +7 -0
- package/dist/tools/global.js +157 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +43 -0
- package/dist/tools/install.d.ts +7 -0
- package/dist/tools/install.js +107 -0
- package/dist/tools/mobile_config.d.ts +10 -0
- package/dist/tools/mobile_config.js +53 -0
- package/dist/tools/parental.d.ts +9 -0
- package/dist/tools/parental.js +42 -0
- package/dist/tools/querylog.d.ts +7 -0
- package/dist/tools/querylog.js +146 -0
- package/dist/tools/rewrites.d.ts +7 -0
- package/dist/tools/rewrites.js +120 -0
- package/dist/tools/safebrowsing.d.ts +7 -0
- package/dist/tools/safebrowsing.js +39 -0
- package/dist/tools/safesearch.d.ts +7 -0
- package/dist/tools/safesearch.js +73 -0
- package/dist/tools/stats.d.ts +7 -0
- package/dist/tools/stats.js +124 -0
- package/dist/tools/tls.d.ts +7 -0
- package/dist/tools/tls.js +175 -0
- package/dist/types/index.d.ts +41 -0
- package/dist/types/index.js +21 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 samik081
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@samik081/mcp-adguard-home)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[](https://nodejs.org)
|
|
4
|
+
|
|
5
|
+
# MCP AdGuard Home
|
|
6
|
+
|
|
7
|
+
MCP server for [AdGuard Home](https://adguard.com/pl/adguard-home/overview.html). Manage DNS filtering, clients, DHCP, rewrites, and more through natural language in Cursor, Claude Code, and Claude Desktop.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **65 tools** across **16 API categories** covering the complete AdGuard Home API
|
|
12
|
+
- **Read-only mode** via `ADGUARD_ACCESS_TIER=read-only` for safe monitoring
|
|
13
|
+
- **Category filtering** via `ADGUARD_CATEGORIES` to expose only the tools you need
|
|
14
|
+
- **Destructive operation guard** with optional confirmation for reset/delete operations
|
|
15
|
+
- **Zero HTTP dependencies** -- uses native `fetch` (Node.js 18+)
|
|
16
|
+
- **TypeScript/ESM** with full type safety
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Run the server directly with npx:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ADGUARD_URL="http://your-adguard-ip:3000" \
|
|
24
|
+
ADGUARD_USERNAME="your-username" \
|
|
25
|
+
ADGUARD_PASSWORD="your-password" \
|
|
26
|
+
npx @samik081/mcp-adguard-home
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The server validates your AdGuard Home connection on startup and fails immediately with a clear error if credentials are missing or invalid.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
**Claude Code CLI (recommended):**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
claude mcp add --transport stdio adguard-home \
|
|
37
|
+
--env ADGUARD_URL=http://your-adguard-ip:3000 \
|
|
38
|
+
--env ADGUARD_USERNAME=your-username \
|
|
39
|
+
--env ADGUARD_PASSWORD=your-password \
|
|
40
|
+
-- npx @samik081/mcp-adguard-home
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**JSON config** (works with Claude Code `.mcp.json`, Claude Desktop `claude_desktop_config.json`, Cursor `.cursor/mcp.json`):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"adguard-home": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["@samik081/mcp-adguard-home"],
|
|
51
|
+
"env": {
|
|
52
|
+
"ADGUARD_URL": "http://your-adguard-ip:3000",
|
|
53
|
+
"ADGUARD_USERNAME": "your-username",
|
|
54
|
+
"ADGUARD_PASSWORD": "your-password"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
| Variable | Required | Default | Description |
|
|
64
|
+
|----------|----------|---------|-------------|
|
|
65
|
+
| `ADGUARD_URL` | Yes | -- | AdGuard Home base URL (e.g., `http://192.168.1.1:3000`) |
|
|
66
|
+
| `ADGUARD_USERNAME` | Yes | -- | Admin username |
|
|
67
|
+
| `ADGUARD_PASSWORD` | Yes | -- | Admin password |
|
|
68
|
+
| `ADGUARD_ACCESS_TIER` | No | `full` | `read-only` for read-only tools only, `full` for all tools |
|
|
69
|
+
| `ADGUARD_CATEGORIES` | No | *(all)* | Comma-separated category allowlist (e.g., `dns,filtering,stats`) |
|
|
70
|
+
| `ADGUARD_CONFIRM_DESTRUCTIVE` | No | `false` | Require `confirm: true` parameter for destructive operations |
|
|
71
|
+
| `DEBUG` | No | `false` | Enable debug logging to stderr |
|
|
72
|
+
|
|
73
|
+
### Available Categories
|
|
74
|
+
|
|
75
|
+
`global`, `dns`, `querylog`, `stats`, `filtering`, `safebrowsing`, `parental`, `safesearch`, `clients`, `dhcp`, `rewrites`, `tls`, `blocked_services`, `access`, `install`, `mobile_config`
|
|
76
|
+
|
|
77
|
+
## Tools
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary>Global (6 tools)</summary>
|
|
81
|
+
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
|------|-------------|
|
|
84
|
+
| `global_get_status` | Retrieve server status including version, DNS addresses, protection state, and ports |
|
|
85
|
+
| `global_get_profile` | Retrieve user profile (name, language, theme) |
|
|
86
|
+
| `global_check_version` | Check for AdGuard Home updates and compare with current version |
|
|
87
|
+
| `global_set_protection` | Enable or disable DNS protection globally, with optional duration for temporary disable |
|
|
88
|
+
| `global_update_profile` | Update user profile settings (name, language, theme) |
|
|
89
|
+
| `global_begin_update` | Initiate an AdGuard Home software update |
|
|
90
|
+
|
|
91
|
+
</details>
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary>DNS (4 tools)</summary>
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `dns_get_info` | Retrieve full DNS configuration including upstreams, cache settings, blocking mode, and DNSSEC |
|
|
99
|
+
| `dns_test_upstream` | Test upstream DNS server configuration to verify servers are reachable |
|
|
100
|
+
| `dns_set_config` | Update DNS server configuration (19 optional fields for partial update) |
|
|
101
|
+
| `dns_clear_cache` | Clear the DNS resolver cache |
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Query Log (4 tools)</summary>
|
|
107
|
+
|
|
108
|
+
| Tool | Description |
|
|
109
|
+
|------|-------------|
|
|
110
|
+
| `querylog_get` | Search DNS query log with optional filtering by response status, search term, and pagination |
|
|
111
|
+
| `querylog_get_config` | Retrieve query log configuration settings |
|
|
112
|
+
| `querylog_set_config` | Update query log configuration (enabled, interval, anonymization) |
|
|
113
|
+
| `querylog_clear` | Clear the entire DNS query log |
|
|
114
|
+
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
<summary>Statistics (4 tools)</summary>
|
|
119
|
+
|
|
120
|
+
| Tool | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `stats_get` | Retrieve DNS statistics including top domains, blocked counts, and client activity |
|
|
123
|
+
| `stats_get_config` | Retrieve statistics configuration settings |
|
|
124
|
+
| `stats_reset` | Reset all DNS statistics |
|
|
125
|
+
| `stats_set_config` | Update statistics configuration (enabled, interval, ignored domains) |
|
|
126
|
+
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary>Filtering (8 tools)</summary>
|
|
131
|
+
|
|
132
|
+
| Tool | Description |
|
|
133
|
+
|------|-------------|
|
|
134
|
+
| `filtering_get_status` | Retrieve filtering configuration including blocklists, allowlists, and user rules |
|
|
135
|
+
| `filtering_check_host` | Test whether a hostname would be blocked by current filtering rules |
|
|
136
|
+
| `filtering_set_config` | Update global filtering configuration (enabled state and update interval) |
|
|
137
|
+
| `filtering_add_url` | Add a new filter URL (blocklist or allowlist) |
|
|
138
|
+
| `filtering_remove_url` | Remove a filter URL from blocklist or allowlist |
|
|
139
|
+
| `filtering_set_url` | Update an existing filter URL (rename, change URL, or enable/disable) |
|
|
140
|
+
| `filtering_refresh` | Force refresh of filter lists to fetch latest updates |
|
|
141
|
+
| `filtering_set_rules` | Set custom filtering rules (replaces all existing custom rules) |
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary>Safe Browsing (2 tools)</summary>
|
|
147
|
+
|
|
148
|
+
| Tool | Description |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `safebrowsing_get_status` | Retrieve safe browsing (malware/phishing protection) status |
|
|
151
|
+
| `safebrowsing_set` | Enable or disable safe browsing protection |
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary>Parental (2 tools)</summary>
|
|
157
|
+
|
|
158
|
+
| Tool | Description |
|
|
159
|
+
|------|-------------|
|
|
160
|
+
| `parental_get_status` | Retrieve parental filtering status |
|
|
161
|
+
| `parental_set` | Enable or disable parental filtering (content restrictions) |
|
|
162
|
+
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
<details>
|
|
166
|
+
<summary>Safe Search (2 tools)</summary>
|
|
167
|
+
|
|
168
|
+
| Tool | Description |
|
|
169
|
+
|------|-------------|
|
|
170
|
+
| `safesearch_get_status` | Retrieve safe search settings showing per-engine enforcement status |
|
|
171
|
+
| `safesearch_set_settings` | Update safe search settings with per-engine configuration (Bing, DuckDuckGo, Google, Pixabay, Yandex, YouTube) |
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary>Clients (5 tools)</summary>
|
|
177
|
+
|
|
178
|
+
| Tool | Description |
|
|
179
|
+
|------|-------------|
|
|
180
|
+
| `clients_get` | Retrieve all configured and auto-detected clients with their settings |
|
|
181
|
+
| `clients_search` | Search for specific clients by their IDs (IP, MAC, CIDR, or client ID) |
|
|
182
|
+
| `clients_add` | Add a new persistent client with per-client settings |
|
|
183
|
+
| `clients_update` | Update an existing persistent client by name |
|
|
184
|
+
| `clients_delete` | Delete a persistent client by name |
|
|
185
|
+
|
|
186
|
+
</details>
|
|
187
|
+
|
|
188
|
+
<details>
|
|
189
|
+
<summary>DHCP (9 tools)</summary>
|
|
190
|
+
|
|
191
|
+
| Tool | Description |
|
|
192
|
+
|------|-------------|
|
|
193
|
+
| `dhcp_get_status` | Retrieve DHCP server configuration, static leases, and active leases |
|
|
194
|
+
| `dhcp_get_interfaces` | Retrieve available network interfaces for DHCP server binding |
|
|
195
|
+
| `dhcp_find_active` | Scan for competing DHCP servers on a network interface |
|
|
196
|
+
| `dhcp_set_config` | Update DHCP server configuration (enabled state, interface, IPv4/IPv6 settings) |
|
|
197
|
+
| `dhcp_add_static_lease` | Add a static DHCP lease mapping a MAC address to an IP |
|
|
198
|
+
| `dhcp_remove_static_lease` | Remove a static DHCP lease |
|
|
199
|
+
| `dhcp_update_static_lease` | Update a static DHCP lease (remove + add pattern) |
|
|
200
|
+
| `dhcp_reset` | Reset DHCP configuration to defaults |
|
|
201
|
+
| `dhcp_reset_leases` | Clear all DHCP leases |
|
|
202
|
+
|
|
203
|
+
</details>
|
|
204
|
+
|
|
205
|
+
<details>
|
|
206
|
+
<summary>Rewrites (6 tools)</summary>
|
|
207
|
+
|
|
208
|
+
| Tool | Description |
|
|
209
|
+
|------|-------------|
|
|
210
|
+
| `rewrites_list` | Retrieve all configured DNS rewrite rules |
|
|
211
|
+
| `rewrites_get_settings` | Retrieve DNS rewrite module enabled/disabled state |
|
|
212
|
+
| `rewrites_add` | Add a new DNS rewrite rule |
|
|
213
|
+
| `rewrites_update` | Update a DNS rewrite rule (remove + add pattern) |
|
|
214
|
+
| `rewrites_delete` | Delete a DNS rewrite rule |
|
|
215
|
+
| `rewrites_set_settings` | Enable or disable the DNS rewrite module |
|
|
216
|
+
|
|
217
|
+
</details>
|
|
218
|
+
|
|
219
|
+
<details>
|
|
220
|
+
<summary>TLS (3 tools)</summary>
|
|
221
|
+
|
|
222
|
+
| Tool | Description |
|
|
223
|
+
|------|-------------|
|
|
224
|
+
| `tls_get_status` | Retrieve TLS configuration and certificate validation status |
|
|
225
|
+
| `tls_validate` | Validate TLS configuration without applying changes |
|
|
226
|
+
| `tls_set_config` | Update TLS configuration including certificates and HTTPS/DoH/DoT settings |
|
|
227
|
+
|
|
228
|
+
</details>
|
|
229
|
+
|
|
230
|
+
<details>
|
|
231
|
+
<summary>Blocked Services (3 tools)</summary>
|
|
232
|
+
|
|
233
|
+
| Tool | Description |
|
|
234
|
+
|------|-------------|
|
|
235
|
+
| `blocked_services_get_all` | List all available services that can be blocked, organized by group |
|
|
236
|
+
| `blocked_services_get` | Retrieve currently blocked services list and schedule |
|
|
237
|
+
| `blocked_services_update` | Update the list of blocked services and optional schedule |
|
|
238
|
+
|
|
239
|
+
</details>
|
|
240
|
+
|
|
241
|
+
<details>
|
|
242
|
+
<summary>Access (2 tools)</summary>
|
|
243
|
+
|
|
244
|
+
| Tool | Description |
|
|
245
|
+
|------|-------------|
|
|
246
|
+
| `access_get_list` | Retrieve access control lists: allowed clients, disallowed clients, and blocked hosts |
|
|
247
|
+
| `access_set_list` | Set access control lists for allowed clients, disallowed clients, and blocked hosts |
|
|
248
|
+
|
|
249
|
+
</details>
|
|
250
|
+
|
|
251
|
+
<details>
|
|
252
|
+
<summary>Install (3 tools)</summary>
|
|
253
|
+
|
|
254
|
+
| Tool | Description |
|
|
255
|
+
|------|-------------|
|
|
256
|
+
| `install_get_addresses` | Retrieve network interface details and ports for initial setup |
|
|
257
|
+
| `install_check_config` | Validate install configuration without applying (checks web/DNS binding, credentials) |
|
|
258
|
+
| `install_apply_config` | Apply initial setup configuration (web/DNS binding and admin credentials) |
|
|
259
|
+
|
|
260
|
+
</details>
|
|
261
|
+
|
|
262
|
+
<details>
|
|
263
|
+
<summary>Mobile Config (2 tools)</summary>
|
|
264
|
+
|
|
265
|
+
| Tool | Description |
|
|
266
|
+
|------|-------------|
|
|
267
|
+
| `mobile_config_get_doh` | Generate Apple .mobileconfig profile for DNS-over-HTTPS |
|
|
268
|
+
| `mobile_config_get_dot` | Generate Apple .mobileconfig profile for DNS-over-TLS |
|
|
269
|
+
|
|
270
|
+
</details>
|
|
271
|
+
|
|
272
|
+
## Verify It Works
|
|
273
|
+
|
|
274
|
+
After configuring your MCP client, ask your AI assistant:
|
|
275
|
+
|
|
276
|
+
> "What's my AdGuard Home server status?"
|
|
277
|
+
|
|
278
|
+
If the connection is working, the assistant will call `global_get_status` and return your server version, DNS addresses, protection state, and port configuration.
|
|
279
|
+
|
|
280
|
+
## Usage Examples
|
|
281
|
+
|
|
282
|
+
- **"What's the current DNS protection status?"** -- calls `global_get_status` to show version, addresses, and protection state.
|
|
283
|
+
- **"Show me all DNS rewrite rules"** -- calls `rewrites_list` to display all configured DNS rewrites.
|
|
284
|
+
- **"Add a DNS rewrite for local.example.com pointing to 192.168.1.100"** -- calls `rewrites_add` to create a new rewrite rule.
|
|
285
|
+
|
|
286
|
+
## Troubleshooting
|
|
287
|
+
|
|
288
|
+
### Connection errors
|
|
289
|
+
|
|
290
|
+
- Verify `ADGUARD_URL` is reachable from the machine running the MCP server
|
|
291
|
+
- Ensure the URL includes the port if non-standard (e.g., `http://192.168.1.1:3000`)
|
|
292
|
+
- Check that AdGuard Home is running and accessible
|
|
293
|
+
|
|
294
|
+
### Authentication failures
|
|
295
|
+
|
|
296
|
+
- Verify `ADGUARD_USERNAME` and `ADGUARD_PASSWORD` are correct
|
|
297
|
+
- Check that the user has admin privileges in AdGuard Home
|
|
298
|
+
|
|
299
|
+
### Tools not showing up
|
|
300
|
+
|
|
301
|
+
- Check your `ADGUARD_ACCESS_TIER` setting -- `read-only` mode only exposes read tools
|
|
302
|
+
- Check `ADGUARD_CATEGORIES` -- only tools in listed categories are registered
|
|
303
|
+
- Verify the server started without errors by checking stderr output
|
|
304
|
+
|
|
305
|
+
## Development
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Install dependencies
|
|
309
|
+
npm install
|
|
310
|
+
|
|
311
|
+
# Build the project
|
|
312
|
+
npm run build
|
|
313
|
+
|
|
314
|
+
# Run in development mode (auto-reload)
|
|
315
|
+
npm run dev
|
|
316
|
+
|
|
317
|
+
# Open the MCP Inspector for interactive testing
|
|
318
|
+
npm run inspect
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication helpers for AdGuard Home HTTP Basic Auth.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create a Basic Auth header value from username and password.
|
|
6
|
+
* Returns the full header value: "Basic <base64>"
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAuthHeader(username: string, password: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication helpers for AdGuard Home HTTP Basic Auth.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create a Basic Auth header value from username and password.
|
|
6
|
+
* Returns the full header value: "Basic <base64>"
|
|
7
|
+
*/
|
|
8
|
+
export function createAuthHeader(username, password) {
|
|
9
|
+
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
|
10
|
+
return `Basic ${encoded}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for AdGuard Home API.
|
|
3
|
+
* Uses native fetch (Node 18+) with Basic Auth, /control/ base path,
|
|
4
|
+
* credential sanitization, and JSON+text response handling.
|
|
5
|
+
*/
|
|
6
|
+
import type { AppConfig } from '../types/index.js';
|
|
7
|
+
export declare class AdGuardClient {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly authHeader;
|
|
10
|
+
private readonly baseUrl;
|
|
11
|
+
constructor(config: AppConfig);
|
|
12
|
+
/**
|
|
13
|
+
* Send a GET request and return parsed JSON or text.
|
|
14
|
+
* JSON responses (Content-Type includes 'json') are parsed; others returned as text.
|
|
15
|
+
*/
|
|
16
|
+
get(path: string): Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Send a POST request with optional JSON body.
|
|
19
|
+
* Returns parsed JSON, text, or empty string for empty responses.
|
|
20
|
+
*/
|
|
21
|
+
post(path: string, body?: unknown): Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Send a GET request and always return raw text response.
|
|
24
|
+
* Used for endpoints that return non-JSON data (e.g., mobile config profiles).
|
|
25
|
+
*/
|
|
26
|
+
getRaw(path: string): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Validate the connection to AdGuard Home by calling GET /control/status.
|
|
29
|
+
* Throws a sanitized, descriptive error on failure.
|
|
30
|
+
*/
|
|
31
|
+
validateConnection(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Parse response based on Content-Type: JSON for 'json' types, text otherwise.
|
|
34
|
+
*/
|
|
35
|
+
private parseResponse;
|
|
36
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for AdGuard Home API.
|
|
3
|
+
* Uses native fetch (Node 18+) with Basic Auth, /control/ base path,
|
|
4
|
+
* credential sanitization, and JSON+text response handling.
|
|
5
|
+
*/
|
|
6
|
+
import { createAuthHeader } from './auth.js';
|
|
7
|
+
import { AdGuardError, sanitizeMessage } from './errors.js';
|
|
8
|
+
/** Request timeout in milliseconds. */
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
export class AdGuardClient {
|
|
11
|
+
config;
|
|
12
|
+
authHeader;
|
|
13
|
+
baseUrl;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.authHeader = createAuthHeader(config.username, config.password);
|
|
17
|
+
this.baseUrl = `${config.url}/control`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Send a GET request and return parsed JSON or text.
|
|
21
|
+
* JSON responses (Content-Type includes 'json') are parsed; others returned as text.
|
|
22
|
+
*/
|
|
23
|
+
async get(path) {
|
|
24
|
+
const url = `${this.baseUrl}/${path}`;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: this.authHeader,
|
|
30
|
+
},
|
|
31
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new AdGuardError(sanitizeMessage(`GET ${path} failed: ${response.status} ${response.statusText}`, this.config), response.status);
|
|
35
|
+
}
|
|
36
|
+
return await this.parseResponse(response);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof AdGuardError)
|
|
40
|
+
throw err;
|
|
41
|
+
throw new AdGuardError(sanitizeMessage(err instanceof Error ? err.message : String(err), this.config));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Send a POST request with optional JSON body.
|
|
46
|
+
* Returns parsed JSON, text, or empty string for empty responses.
|
|
47
|
+
*/
|
|
48
|
+
async post(path, body) {
|
|
49
|
+
const url = `${this.baseUrl}/${path}`;
|
|
50
|
+
const headers = {
|
|
51
|
+
Authorization: this.authHeader,
|
|
52
|
+
};
|
|
53
|
+
if (body !== undefined) {
|
|
54
|
+
headers['Content-Type'] = 'application/json';
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers,
|
|
60
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
61
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new AdGuardError(sanitizeMessage(`POST ${path} failed: ${response.status} ${response.statusText}`, this.config), response.status);
|
|
65
|
+
}
|
|
66
|
+
// Some AdGuard POST endpoints return empty 200
|
|
67
|
+
const contentLength = response.headers.get('content-length');
|
|
68
|
+
if (contentLength === '0') {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
// Try to parse response body; return empty string if no body
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
if (!text)
|
|
74
|
+
return '';
|
|
75
|
+
// Attempt JSON parse if content type suggests it
|
|
76
|
+
const contentType = response.headers.get('content-type') || '';
|
|
77
|
+
if (contentType.includes('json')) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(text);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err instanceof AdGuardError)
|
|
89
|
+
throw err;
|
|
90
|
+
throw new AdGuardError(sanitizeMessage(err instanceof Error ? err.message : String(err), this.config));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Send a GET request and always return raw text response.
|
|
95
|
+
* Used for endpoints that return non-JSON data (e.g., mobile config profiles).
|
|
96
|
+
*/
|
|
97
|
+
async getRaw(path) {
|
|
98
|
+
const url = `${this.baseUrl}/${path}`;
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(url, {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: this.authHeader,
|
|
104
|
+
},
|
|
105
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
106
|
+
});
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new AdGuardError(sanitizeMessage(`GET ${path} failed: ${response.status} ${response.statusText}`, this.config), response.status);
|
|
109
|
+
}
|
|
110
|
+
return await response.text();
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof AdGuardError)
|
|
114
|
+
throw err;
|
|
115
|
+
throw new AdGuardError(sanitizeMessage(err instanceof Error ? err.message : String(err), this.config));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate the connection to AdGuard Home by calling GET /control/status.
|
|
120
|
+
* Throws a sanitized, descriptive error on failure.
|
|
121
|
+
*/
|
|
122
|
+
async validateConnection() {
|
|
123
|
+
const url = `${this.baseUrl}/status`;
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url, {
|
|
126
|
+
method: 'GET',
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: this.authHeader,
|
|
129
|
+
},
|
|
130
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
131
|
+
});
|
|
132
|
+
if (response.status === 401 || response.status === 403) {
|
|
133
|
+
throw new AdGuardError('Authentication failed -- check ADGUARD_USERNAME and ADGUARD_PASSWORD', response.status);
|
|
134
|
+
}
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new AdGuardError(sanitizeMessage(`Connection check failed: ${response.status} ${response.statusText}`, this.config), response.status);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (err instanceof AdGuardError)
|
|
141
|
+
throw err;
|
|
142
|
+
throw new AdGuardError(sanitizeMessage(`Cannot connect to AdGuard Home at ${this.config.url}: ${err instanceof Error ? err.message : String(err)}`, this.config));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse response based on Content-Type: JSON for 'json' types, text otherwise.
|
|
147
|
+
*/
|
|
148
|
+
async parseResponse(response) {
|
|
149
|
+
const contentType = response.headers.get('content-type') || '';
|
|
150
|
+
if (contentType.includes('json')) {
|
|
151
|
+
return await response.json();
|
|
152
|
+
}
|
|
153
|
+
return await response.text();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable parsing and validation.
|
|
3
|
+
* Reads required and optional config from process.env.
|
|
4
|
+
*/
|
|
5
|
+
import type { AppConfig } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Load and validate application config from environment variables.
|
|
8
|
+
*
|
|
9
|
+
* Required: ADGUARD_URL, ADGUARD_USERNAME, ADGUARD_PASSWORD
|
|
10
|
+
* Optional: ADGUARD_ACCESS_TIER (default: 'full'), ADGUARD_CATEGORIES (comma-separated), DEBUG
|
|
11
|
+
*
|
|
12
|
+
* Throws clear error (no credentials in message) if required vars are missing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadConfig(): AppConfig;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable parsing and validation.
|
|
3
|
+
* Reads required and optional config from process.env.
|
|
4
|
+
*/
|
|
5
|
+
import { VALID_CATEGORIES } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Load and validate application config from environment variables.
|
|
8
|
+
*
|
|
9
|
+
* Required: ADGUARD_URL, ADGUARD_USERNAME, ADGUARD_PASSWORD
|
|
10
|
+
* Optional: ADGUARD_ACCESS_TIER (default: 'full'), ADGUARD_CATEGORIES (comma-separated), DEBUG
|
|
11
|
+
*
|
|
12
|
+
* Throws clear error (no credentials in message) if required vars are missing.
|
|
13
|
+
*/
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
const url = process.env.ADGUARD_URL;
|
|
16
|
+
const username = process.env.ADGUARD_USERNAME;
|
|
17
|
+
const password = process.env.ADGUARD_PASSWORD;
|
|
18
|
+
// Validate required vars
|
|
19
|
+
const missing = [];
|
|
20
|
+
if (!url)
|
|
21
|
+
missing.push('ADGUARD_URL');
|
|
22
|
+
if (!username)
|
|
23
|
+
missing.push('ADGUARD_USERNAME');
|
|
24
|
+
if (!password)
|
|
25
|
+
missing.push('ADGUARD_PASSWORD');
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
throw new Error(`Missing required environment variables: ${missing.join(', ')}. ` +
|
|
28
|
+
'Set these variables to connect to your AdGuard Home instance.');
|
|
29
|
+
}
|
|
30
|
+
// Parse access tier
|
|
31
|
+
const tierRaw = process.env.ADGUARD_ACCESS_TIER;
|
|
32
|
+
let accessTier = 'full';
|
|
33
|
+
if (tierRaw) {
|
|
34
|
+
const normalized = tierRaw.toLowerCase().trim();
|
|
35
|
+
if (normalized !== 'read-only' && normalized !== 'full') {
|
|
36
|
+
throw new Error(`Invalid ADGUARD_ACCESS_TIER: "${tierRaw}". Must be "read-only" or "full".`);
|
|
37
|
+
}
|
|
38
|
+
accessTier = normalized;
|
|
39
|
+
}
|
|
40
|
+
// Parse categories
|
|
41
|
+
const categoriesRaw = process.env.ADGUARD_CATEGORIES;
|
|
42
|
+
let categories = null;
|
|
43
|
+
if (categoriesRaw) {
|
|
44
|
+
const parsed = categoriesRaw
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((c) => c.trim().toLowerCase())
|
|
47
|
+
.filter((c) => c.length > 0);
|
|
48
|
+
// Validate each category
|
|
49
|
+
const invalid = parsed.filter((c) => !VALID_CATEGORIES.includes(c));
|
|
50
|
+
if (invalid.length > 0) {
|
|
51
|
+
throw new Error(`Invalid ADGUARD_CATEGORIES: ${invalid.map((c) => `"${c}"`).join(', ')}. ` +
|
|
52
|
+
`Valid categories: ${VALID_CATEGORIES.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
categories = parsed;
|
|
55
|
+
}
|
|
56
|
+
// Parse debug flag
|
|
57
|
+
const debug = Boolean(process.env.DEBUG);
|
|
58
|
+
// Parse destructive confirmation flag
|
|
59
|
+
const confirmDestructive = process.env.ADGUARD_CONFIRM_DESTRUCTIVE?.toLowerCase() === 'true';
|
|
60
|
+
return {
|
|
61
|
+
url: url.replace(/\/+$/, ''), // Strip trailing slashes
|
|
62
|
+
username: username,
|
|
63
|
+
password: password,
|
|
64
|
+
accessTier,
|
|
65
|
+
categories,
|
|
66
|
+
debug,
|
|
67
|
+
confirmDestructive,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling with credential sanitization.
|
|
3
|
+
* All error messages are scrubbed of credentials before being
|
|
4
|
+
* exposed to logs or LLM context.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize a message by replacing any occurrence of credentials
|
|
8
|
+
* (username, password, or Base64 auth string) with [REDACTED].
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizeMessage(message: string, config: {
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Custom error class for AdGuard Home API errors.
|
|
16
|
+
*/
|
|
17
|
+
export declare class AdGuardError extends Error {
|
|
18
|
+
statusCode?: number;
|
|
19
|
+
constructor(message: string, statusCode?: number);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wrap any error with a sanitized message.
|
|
23
|
+
* Handles Error instances, strings, and unknown thrown values.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSafeError(err: unknown, config: {
|
|
26
|
+
username: string;
|
|
27
|
+
password: string;
|
|
28
|
+
}): Error;
|