iflow-mcp_enuno-unifi-mcp-server 0.2.1__py3-none-any.whl
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.
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
- src/__init__.py +3 -0
- src/__main__.py +6 -0
- src/api/__init__.py +5 -0
- src/api/client.py +727 -0
- src/api/site_manager_client.py +176 -0
- src/cache.py +483 -0
- src/config/__init__.py +5 -0
- src/config/config.py +321 -0
- src/main.py +2234 -0
- src/models/__init__.py +126 -0
- src/models/acl.py +41 -0
- src/models/backup.py +272 -0
- src/models/client.py +74 -0
- src/models/device.py +53 -0
- src/models/dpi.py +50 -0
- src/models/firewall_policy.py +123 -0
- src/models/firewall_zone.py +28 -0
- src/models/network.py +62 -0
- src/models/qos_profile.py +458 -0
- src/models/radius.py +141 -0
- src/models/reference_data.py +34 -0
- src/models/site.py +59 -0
- src/models/site_manager.py +120 -0
- src/models/topology.py +138 -0
- src/models/traffic_flow.py +137 -0
- src/models/traffic_matching_list.py +56 -0
- src/models/voucher.py +42 -0
- src/models/vpn.py +73 -0
- src/models/wan.py +48 -0
- src/models/zbf_matrix.py +49 -0
- src/resources/__init__.py +8 -0
- src/resources/clients.py +111 -0
- src/resources/devices.py +102 -0
- src/resources/networks.py +93 -0
- src/resources/site_manager.py +64 -0
- src/resources/sites.py +86 -0
- src/tools/__init__.py +25 -0
- src/tools/acls.py +328 -0
- src/tools/application.py +42 -0
- src/tools/backups.py +1173 -0
- src/tools/client_management.py +505 -0
- src/tools/clients.py +203 -0
- src/tools/device_control.py +325 -0
- src/tools/devices.py +354 -0
- src/tools/dpi.py +241 -0
- src/tools/dpi_tools.py +89 -0
- src/tools/firewall.py +417 -0
- src/tools/firewall_policies.py +430 -0
- src/tools/firewall_zones.py +515 -0
- src/tools/network_config.py +388 -0
- src/tools/networks.py +190 -0
- src/tools/port_forwarding.py +263 -0
- src/tools/qos.py +1070 -0
- src/tools/radius.py +763 -0
- src/tools/reference_data.py +107 -0
- src/tools/site_manager.py +466 -0
- src/tools/site_vpn.py +95 -0
- src/tools/sites.py +187 -0
- src/tools/topology.py +406 -0
- src/tools/traffic_flows.py +1062 -0
- src/tools/traffic_matching_lists.py +371 -0
- src/tools/vouchers.py +249 -0
- src/tools/vpn.py +76 -0
- src/tools/wans.py +30 -0
- src/tools/wifi.py +498 -0
- src/tools/zbf_matrix.py +326 -0
- src/utils/__init__.py +88 -0
- src/utils/audit.py +213 -0
- src/utils/exceptions.py +114 -0
- src/utils/helpers.py +159 -0
- src/utils/logger.py +105 -0
- src/utils/sanitize.py +244 -0
- src/utils/validators.py +160 -0
- src/webhooks/__init__.py +6 -0
- src/webhooks/handlers.py +196 -0
- src/webhooks/receiver.py +290 -0
src/main.py
ADDED
|
@@ -0,0 +1,2234 @@
|
|
|
1
|
+
"""Main entry point for UniFi MCP Server."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from agnost import config as agnost_config
|
|
6
|
+
from agnost import track
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from .config import Settings
|
|
10
|
+
from .resources import ClientsResource, DevicesResource, NetworksResource, SitesResource
|
|
11
|
+
from .resources import site_manager as site_manager_resource
|
|
12
|
+
from .tools import acls as acls_tools
|
|
13
|
+
from .tools import application as application_tools
|
|
14
|
+
from .tools import backups as backups_tools
|
|
15
|
+
from .tools import client_management as client_mgmt_tools
|
|
16
|
+
from .tools import clients as clients_tools
|
|
17
|
+
from .tools import device_control as device_control_tools
|
|
18
|
+
from .tools import devices as devices_tools
|
|
19
|
+
from .tools import dpi as dpi_tools
|
|
20
|
+
from .tools import dpi_tools as dpi_new_tools
|
|
21
|
+
from .tools import firewall as firewall_tools
|
|
22
|
+
from .tools import firewall_zones as firewall_zones_tools
|
|
23
|
+
from .tools import network_config as network_config_tools
|
|
24
|
+
from .tools import networks as networks_tools
|
|
25
|
+
from .tools import port_forwarding as port_fwd_tools
|
|
26
|
+
from .tools import qos as qos_tools
|
|
27
|
+
from .tools import radius as radius_tools
|
|
28
|
+
from .tools import reference_data as ref_tools
|
|
29
|
+
from .tools import site_manager as site_manager_tools
|
|
30
|
+
from .tools import site_vpn as site_vpn_tools
|
|
31
|
+
from .tools import sites as sites_tools
|
|
32
|
+
from .tools import topology as topology_tools
|
|
33
|
+
from .tools import traffic_flows as traffic_flows_tools
|
|
34
|
+
from .tools import traffic_matching_lists as tml_tools
|
|
35
|
+
from .tools import vouchers as vouchers_tools
|
|
36
|
+
from .tools import vpn as vpn_tools
|
|
37
|
+
from .tools import wans as wans_tools
|
|
38
|
+
from .tools import wifi as wifi_tools
|
|
39
|
+
from .utils import get_logger
|
|
40
|
+
|
|
41
|
+
# Initialize settings
|
|
42
|
+
settings = Settings()
|
|
43
|
+
logger = get_logger(__name__, settings.log_level)
|
|
44
|
+
|
|
45
|
+
# Initialize FastMCP server
|
|
46
|
+
mcp = FastMCP("UniFi MCP Server")
|
|
47
|
+
|
|
48
|
+
# Configure agnost tracking if enabled
|
|
49
|
+
if os.getenv("AGNOST_ENABLED", "false").lower() in ("true", "1", "yes"):
|
|
50
|
+
agnost_org_id = os.getenv("AGNOST_ORG_ID")
|
|
51
|
+
if agnost_org_id:
|
|
52
|
+
try:
|
|
53
|
+
# Configure tracking with input/output control
|
|
54
|
+
disable_input = os.getenv("AGNOST_DISABLE_INPUT", "false").lower() in (
|
|
55
|
+
"true",
|
|
56
|
+
"1",
|
|
57
|
+
"yes",
|
|
58
|
+
)
|
|
59
|
+
disable_output = os.getenv("AGNOST_DISABLE_OUTPUT", "false").lower() in (
|
|
60
|
+
"true",
|
|
61
|
+
"1",
|
|
62
|
+
"yes",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
track(
|
|
66
|
+
mcp,
|
|
67
|
+
agnost_org_id,
|
|
68
|
+
agnost_config(
|
|
69
|
+
endpoint=os.getenv("AGNOST_ENDPOINT", "https://api.agnost.ai"),
|
|
70
|
+
disable_input=disable_input,
|
|
71
|
+
disable_output=disable_output,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
logger.info(
|
|
75
|
+
f"Agnost.ai performance tracking enabled (input: {not disable_input}, output: {not disable_output})"
|
|
76
|
+
)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(f"Failed to initialize agnost tracking: {e}")
|
|
79
|
+
else:
|
|
80
|
+
logger.warning("AGNOST_ENABLED is true but AGNOST_ORG_ID is not set")
|
|
81
|
+
|
|
82
|
+
# Initialize resource handlers
|
|
83
|
+
sites_resource = SitesResource(settings)
|
|
84
|
+
devices_resource = DevicesResource(settings)
|
|
85
|
+
clients_resource = ClientsResource(settings)
|
|
86
|
+
networks_resource = NetworksResource(settings)
|
|
87
|
+
site_manager_res = site_manager_resource.SiteManagerResource(settings)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# MCP Tools
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
async def health_check() -> dict[str, str]:
|
|
93
|
+
"""Health check endpoint to verify server is running.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Status information
|
|
97
|
+
"""
|
|
98
|
+
return {
|
|
99
|
+
"status": "healthy",
|
|
100
|
+
"version": "0.2.0",
|
|
101
|
+
"api_type": settings.api_type.value,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Register debug tool only if DEBUG is enabled
|
|
106
|
+
if os.getenv("DEBUG", "").lower() in ("true", "1", "yes"):
|
|
107
|
+
|
|
108
|
+
@mcp.tool()
|
|
109
|
+
async def debug_api_request(endpoint: str, method: str = "GET") -> dict:
|
|
110
|
+
"""Debug tool to query arbitrary UniFi API endpoints.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
endpoint: API endpoint path (e.g., /proxy/network/api/s/default/rest/networkconf)
|
|
114
|
+
method: HTTP method (GET, POST, PUT, DELETE)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Raw JSON response from the API
|
|
118
|
+
"""
|
|
119
|
+
from .api import UniFiClient
|
|
120
|
+
|
|
121
|
+
async with UniFiClient(settings) as client:
|
|
122
|
+
await client.authenticate()
|
|
123
|
+
if method.upper() == "GET":
|
|
124
|
+
return await client.get(endpoint)
|
|
125
|
+
elif method.upper() == "DELETE":
|
|
126
|
+
return await client.delete(endpoint)
|
|
127
|
+
else:
|
|
128
|
+
return {"error": f"Method {method} requires json_data parameter (not implemented)"}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# MCP Resources
|
|
132
|
+
@mcp.resource("sites://")
|
|
133
|
+
async def get_sites_resource() -> str:
|
|
134
|
+
"""Get all UniFi sites.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
JSON string of sites list
|
|
138
|
+
"""
|
|
139
|
+
sites = await sites_resource.list_sites()
|
|
140
|
+
return "\n".join([f"Site: {s.name} ({s.id})" for s in sites])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@mcp.resource("sites://{site_id}/devices")
|
|
144
|
+
async def get_devices_resource(site_id: str) -> str:
|
|
145
|
+
"""Get all devices for a site.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
site_id: Site identifier
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
JSON string of devices list
|
|
152
|
+
"""
|
|
153
|
+
devices = await devices_resource.list_devices(site_id)
|
|
154
|
+
return "\n".join([f"Device: {d.name or d.model} ({d.mac}) - {d.ip}" for d in devices])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.resource("sites://{site_id}/clients")
|
|
158
|
+
async def get_clients_resource(site_id: str) -> str:
|
|
159
|
+
"""Get all clients for a site.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
site_id: Site identifier
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
JSON string of clients list
|
|
166
|
+
"""
|
|
167
|
+
clients = await clients_resource.list_clients(site_id, active_only=True)
|
|
168
|
+
return "\n".join([f"Client: {c.hostname or c.name or c.mac} ({c.ip})" for c in clients])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@mcp.resource("sites://{site_id}/networks")
|
|
172
|
+
async def get_networks_resource(site_id: str) -> str:
|
|
173
|
+
"""Get all networks for a site.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
site_id: Site identifier
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
JSON string of networks list
|
|
180
|
+
"""
|
|
181
|
+
networks = await networks_resource.list_networks(site_id)
|
|
182
|
+
return "\n".join(
|
|
183
|
+
[f"Network: {n.name} (VLAN {n.vlan_id or 'none'}) - {n.ip_subnet}" for n in networks]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Device Management Tools
|
|
188
|
+
@mcp.tool()
|
|
189
|
+
async def get_device_details(site_id: str, device_id: str) -> dict:
|
|
190
|
+
"""Get detailed information for a specific device."""
|
|
191
|
+
return await devices_tools.get_device_details(site_id, device_id, settings)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@mcp.tool()
|
|
195
|
+
async def get_device_statistics(site_id: str, device_id: str) -> dict:
|
|
196
|
+
"""Retrieve real-time statistics for a device."""
|
|
197
|
+
return await devices_tools.get_device_statistics(site_id, device_id, settings)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@mcp.tool()
|
|
201
|
+
async def list_devices_by_type(site_id: str, device_type: str) -> list[dict]:
|
|
202
|
+
"""Filter devices by type (uap, usw, ugw)."""
|
|
203
|
+
return await devices_tools.list_devices_by_type(site_id, device_type, settings)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
async def search_devices(site_id: str, query: str) -> list[dict]:
|
|
208
|
+
"""Search devices by name, MAC, or IP address."""
|
|
209
|
+
return await devices_tools.search_devices(site_id, query, settings)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Client Management Tools
|
|
213
|
+
@mcp.tool()
|
|
214
|
+
async def get_client_details(site_id: str, client_mac: str) -> dict:
|
|
215
|
+
"""Get detailed information for a specific client."""
|
|
216
|
+
return await clients_tools.get_client_details(site_id, client_mac, settings)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@mcp.tool()
|
|
220
|
+
async def get_client_statistics(site_id: str, client_mac: str) -> dict:
|
|
221
|
+
"""Retrieve bandwidth and connection statistics for a client."""
|
|
222
|
+
return await clients_tools.get_client_statistics(site_id, client_mac, settings)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def list_active_clients(site_id: str) -> list[dict]:
|
|
227
|
+
"""List currently connected clients."""
|
|
228
|
+
return await clients_tools.list_active_clients(site_id, settings)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@mcp.tool()
|
|
232
|
+
async def search_clients(site_id: str, query: str) -> list[dict]:
|
|
233
|
+
"""Search clients by MAC, IP, or hostname."""
|
|
234
|
+
return await clients_tools.search_clients(site_id, query, settings)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Network Information Tools
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
async def get_network_details(site_id: str, network_id: str) -> dict:
|
|
240
|
+
"""Get detailed network configuration."""
|
|
241
|
+
return await networks_tools.get_network_details(site_id, network_id, settings)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@mcp.tool()
|
|
245
|
+
async def list_vlans(site_id: str) -> list[dict]:
|
|
246
|
+
"""List all VLANs in a site."""
|
|
247
|
+
return await networks_tools.list_vlans(site_id, settings)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@mcp.tool()
|
|
251
|
+
async def get_subnet_info(site_id: str, network_id: str) -> dict:
|
|
252
|
+
"""Get subnet and DHCP information for a network."""
|
|
253
|
+
return await networks_tools.get_subnet_info(site_id, network_id, settings)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@mcp.tool()
|
|
257
|
+
async def get_network_statistics(site_id: str) -> dict:
|
|
258
|
+
"""Retrieve network usage statistics for a site."""
|
|
259
|
+
return await networks_tools.get_network_statistics(site_id, settings)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# Site Management Tools
|
|
263
|
+
@mcp.tool()
|
|
264
|
+
async def get_site_details(site_id: str) -> dict:
|
|
265
|
+
"""Get detailed site information."""
|
|
266
|
+
return await sites_tools.get_site_details(site_id, settings)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
async def list_all_sites() -> list[dict]:
|
|
271
|
+
"""List all accessible sites."""
|
|
272
|
+
return await sites_tools.list_sites(settings)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@mcp.tool()
|
|
276
|
+
async def get_site_statistics(site_id: str) -> dict:
|
|
277
|
+
"""Retrieve site-wide statistics."""
|
|
278
|
+
return await sites_tools.get_site_statistics(site_id, settings)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Firewall Management Tools (Phase 4)
|
|
282
|
+
@mcp.tool()
|
|
283
|
+
async def list_firewall_rules(site_id: str) -> list[dict]:
|
|
284
|
+
"""List all firewall rules in a site."""
|
|
285
|
+
return await firewall_tools.list_firewall_rules(site_id, settings)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@mcp.tool()
|
|
289
|
+
async def create_firewall_rule(
|
|
290
|
+
site_id: str,
|
|
291
|
+
name: str,
|
|
292
|
+
action: str,
|
|
293
|
+
source: str | None = None,
|
|
294
|
+
destination: str | None = None,
|
|
295
|
+
protocol: str | None = None,
|
|
296
|
+
port: int | None = None,
|
|
297
|
+
enabled: bool = True,
|
|
298
|
+
confirm: bool = False,
|
|
299
|
+
dry_run: bool = False,
|
|
300
|
+
) -> dict:
|
|
301
|
+
"""Create a new firewall rule (requires confirm=True)."""
|
|
302
|
+
return await firewall_tools.create_firewall_rule(
|
|
303
|
+
site_id,
|
|
304
|
+
name,
|
|
305
|
+
action,
|
|
306
|
+
settings,
|
|
307
|
+
source,
|
|
308
|
+
destination,
|
|
309
|
+
protocol,
|
|
310
|
+
port,
|
|
311
|
+
enabled,
|
|
312
|
+
confirm,
|
|
313
|
+
dry_run,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool()
|
|
318
|
+
async def update_firewall_rule(
|
|
319
|
+
site_id: str,
|
|
320
|
+
rule_id: str,
|
|
321
|
+
name: str | None = None,
|
|
322
|
+
action: str | None = None,
|
|
323
|
+
source: str | None = None,
|
|
324
|
+
destination: str | None = None,
|
|
325
|
+
protocol: str | None = None,
|
|
326
|
+
port: int | None = None,
|
|
327
|
+
enabled: bool | None = None,
|
|
328
|
+
confirm: bool = False,
|
|
329
|
+
dry_run: bool = False,
|
|
330
|
+
) -> dict:
|
|
331
|
+
"""Update an existing firewall rule (requires confirm=True)."""
|
|
332
|
+
return await firewall_tools.update_firewall_rule(
|
|
333
|
+
site_id,
|
|
334
|
+
rule_id,
|
|
335
|
+
settings,
|
|
336
|
+
name,
|
|
337
|
+
action,
|
|
338
|
+
source,
|
|
339
|
+
destination,
|
|
340
|
+
protocol,
|
|
341
|
+
port,
|
|
342
|
+
enabled,
|
|
343
|
+
confirm,
|
|
344
|
+
dry_run,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@mcp.tool()
|
|
349
|
+
async def delete_firewall_rule(
|
|
350
|
+
site_id: str, rule_id: str, confirm: bool = False, dry_run: bool = False
|
|
351
|
+
) -> dict:
|
|
352
|
+
"""Delete a firewall rule (requires confirm=True)."""
|
|
353
|
+
return await firewall_tools.delete_firewall_rule(site_id, rule_id, settings, confirm, dry_run)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Backup and Restore Tools (Phase 4)
|
|
357
|
+
@mcp.tool()
|
|
358
|
+
async def trigger_backup(
|
|
359
|
+
site_id: str,
|
|
360
|
+
backup_type: str,
|
|
361
|
+
retention_days: int = 30,
|
|
362
|
+
confirm: bool = False,
|
|
363
|
+
dry_run: bool = False,
|
|
364
|
+
) -> dict:
|
|
365
|
+
"""Trigger a backup operation on the UniFi controller (requires confirm=True).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
site_id: Site identifier
|
|
369
|
+
backup_type: Type of backup ("network" or "system")
|
|
370
|
+
retention_days: Number of days to retain the backup (default: 30, -1 for indefinite)
|
|
371
|
+
confirm: Confirmation flag (must be True to execute)
|
|
372
|
+
dry_run: If True, validate but don't create the backup
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Backup operation result including download URL and metadata
|
|
376
|
+
"""
|
|
377
|
+
return await backups_tools.trigger_backup(
|
|
378
|
+
site_id, backup_type, settings, retention_days, confirm, dry_run
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@mcp.tool()
|
|
383
|
+
async def list_backups(site_id: str) -> list[dict]:
|
|
384
|
+
"""List all available backups for a site.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
site_id: Site identifier
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
List of backup metadata dictionaries
|
|
391
|
+
"""
|
|
392
|
+
return await backups_tools.list_backups(site_id, settings)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@mcp.tool()
|
|
396
|
+
async def get_backup_details(site_id: str, backup_filename: str) -> dict:
|
|
397
|
+
"""Get detailed information about a specific backup.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
site_id: Site identifier
|
|
401
|
+
backup_filename: Backup filename (e.g., "backup_2025-01-29.unf")
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Detailed backup metadata dictionary
|
|
405
|
+
"""
|
|
406
|
+
return await backups_tools.get_backup_details(site_id, backup_filename, settings)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@mcp.tool()
|
|
410
|
+
async def download_backup(
|
|
411
|
+
site_id: str,
|
|
412
|
+
backup_filename: str,
|
|
413
|
+
output_path: str,
|
|
414
|
+
verify_checksum: bool = True,
|
|
415
|
+
) -> dict:
|
|
416
|
+
"""Download a backup file to local storage.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
site_id: Site identifier
|
|
420
|
+
backup_filename: Backup filename to download
|
|
421
|
+
output_path: Local filesystem path to save the backup
|
|
422
|
+
verify_checksum: Whether to calculate and verify file checksum
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Download result with file path and metadata
|
|
426
|
+
"""
|
|
427
|
+
return await backups_tools.download_backup(
|
|
428
|
+
site_id, backup_filename, output_path, settings, verify_checksum
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@mcp.tool()
|
|
433
|
+
async def delete_backup(
|
|
434
|
+
site_id: str,
|
|
435
|
+
backup_filename: str,
|
|
436
|
+
confirm: bool = False,
|
|
437
|
+
dry_run: bool = False,
|
|
438
|
+
) -> dict:
|
|
439
|
+
"""Delete a backup file from the controller (requires confirm=True).
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
site_id: Site identifier
|
|
443
|
+
backup_filename: Backup filename to delete
|
|
444
|
+
confirm: Confirmation flag (must be True to execute)
|
|
445
|
+
dry_run: If True, validate but don't delete the backup
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Deletion result
|
|
449
|
+
|
|
450
|
+
Warning:
|
|
451
|
+
This operation permanently deletes the backup file.
|
|
452
|
+
"""
|
|
453
|
+
return await backups_tools.delete_backup(site_id, backup_filename, settings, confirm, dry_run)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@mcp.tool()
|
|
457
|
+
async def restore_backup(
|
|
458
|
+
site_id: str,
|
|
459
|
+
backup_filename: str,
|
|
460
|
+
create_pre_restore_backup: bool = True,
|
|
461
|
+
confirm: bool = False,
|
|
462
|
+
dry_run: bool = False,
|
|
463
|
+
) -> dict:
|
|
464
|
+
"""Restore the UniFi controller from a backup file (requires confirm=True).
|
|
465
|
+
|
|
466
|
+
This is a DESTRUCTIVE operation that will restore the controller to the state
|
|
467
|
+
captured in the backup. The controller may restart during the restore process.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
site_id: Site identifier
|
|
471
|
+
backup_filename: Backup filename to restore from
|
|
472
|
+
create_pre_restore_backup: Create automatic backup before restore (recommended)
|
|
473
|
+
confirm: Confirmation flag (must be True to execute)
|
|
474
|
+
dry_run: If True, validate but don't restore
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Restore operation result including pre-restore backup info
|
|
478
|
+
|
|
479
|
+
Warning:
|
|
480
|
+
This operation will restore all configuration from the backup and may
|
|
481
|
+
cause controller restart. ALWAYS use create_pre_restore_backup=True
|
|
482
|
+
for safety.
|
|
483
|
+
"""
|
|
484
|
+
return await backups_tools.restore_backup(
|
|
485
|
+
site_id, backup_filename, settings, create_pre_restore_backup, confirm, dry_run
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@mcp.tool()
|
|
490
|
+
async def validate_backup(site_id: str, backup_filename: str) -> dict:
|
|
491
|
+
"""Validate a backup file before restore.
|
|
492
|
+
|
|
493
|
+
Performs integrity checks on a backup file to ensure it's valid and compatible
|
|
494
|
+
with the current controller version.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
site_id: Site identifier
|
|
498
|
+
backup_filename: Backup filename to validate
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Validation result with details and warnings
|
|
502
|
+
"""
|
|
503
|
+
return await backups_tools.validate_backup(site_id, backup_filename, settings)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@mcp.tool()
|
|
507
|
+
async def get_backup_status(operation_id: str) -> dict:
|
|
508
|
+
"""Get the status of an ongoing or completed backup operation.
|
|
509
|
+
|
|
510
|
+
Monitor the progress of a backup operation. Useful for tracking long-running
|
|
511
|
+
system backups.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
operation_id: Backup operation identifier (returned by trigger_backup)
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Backup operation status including progress and result
|
|
518
|
+
"""
|
|
519
|
+
return await backups_tools.get_backup_status(operation_id, settings)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@mcp.tool()
|
|
523
|
+
async def get_restore_status(operation_id: str) -> dict:
|
|
524
|
+
"""Get the status of an ongoing or completed restore operation.
|
|
525
|
+
|
|
526
|
+
Monitor the progress of a restore operation. Critical for tracking restore
|
|
527
|
+
progress as controller may restart during restore.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
operation_id: Restore operation identifier (returned by restore_backup)
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Restore operation status with rollback availability
|
|
534
|
+
"""
|
|
535
|
+
return await backups_tools.get_restore_status(operation_id, settings)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@mcp.tool()
|
|
539
|
+
async def schedule_backups(
|
|
540
|
+
site_id: str,
|
|
541
|
+
backup_type: str,
|
|
542
|
+
frequency: str,
|
|
543
|
+
time_of_day: str,
|
|
544
|
+
enabled: bool = True,
|
|
545
|
+
retention_days: int = 30,
|
|
546
|
+
max_backups: int = 10,
|
|
547
|
+
day_of_week: int | None = None,
|
|
548
|
+
day_of_month: int | None = None,
|
|
549
|
+
cloud_backup_enabled: bool = False,
|
|
550
|
+
confirm: bool = False,
|
|
551
|
+
dry_run: bool = False,
|
|
552
|
+
) -> dict:
|
|
553
|
+
"""Configure automated backup schedule (requires confirm=True).
|
|
554
|
+
|
|
555
|
+
Set up recurring backups to run automatically at specified intervals.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
site_id: Site identifier
|
|
559
|
+
backup_type: "network" or "system"
|
|
560
|
+
frequency: "daily", "weekly", or "monthly"
|
|
561
|
+
time_of_day: Time in HH:MM format (24-hour)
|
|
562
|
+
enabled: Whether schedule is enabled (default: True)
|
|
563
|
+
retention_days: Days to retain backups (1-365, default: 30)
|
|
564
|
+
max_backups: Maximum backups to keep (1-100, default: 10)
|
|
565
|
+
day_of_week: For weekly: 0=Monday, 6=Sunday
|
|
566
|
+
day_of_month: For monthly: 1-31
|
|
567
|
+
cloud_backup_enabled: Sync to cloud (default: False)
|
|
568
|
+
confirm: Must be True to execute
|
|
569
|
+
dry_run: Validate without configuring (default: False)
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Backup schedule configuration details
|
|
573
|
+
"""
|
|
574
|
+
return await backups_tools.schedule_backups(
|
|
575
|
+
site_id,
|
|
576
|
+
backup_type,
|
|
577
|
+
frequency,
|
|
578
|
+
time_of_day,
|
|
579
|
+
settings,
|
|
580
|
+
enabled,
|
|
581
|
+
retention_days,
|
|
582
|
+
max_backups,
|
|
583
|
+
day_of_week,
|
|
584
|
+
day_of_month,
|
|
585
|
+
cloud_backup_enabled,
|
|
586
|
+
confirm,
|
|
587
|
+
dry_run,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@mcp.tool()
|
|
592
|
+
async def get_backup_schedule(site_id: str) -> dict:
|
|
593
|
+
"""Get the configured automated backup schedule for a site.
|
|
594
|
+
|
|
595
|
+
Retrieve details about the current backup schedule including frequency,
|
|
596
|
+
retention policy, and next scheduled execution.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
site_id: Site identifier
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Backup schedule configuration, or indication if no schedule exists
|
|
603
|
+
"""
|
|
604
|
+
return await backups_tools.get_backup_schedule(site_id, settings)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# Network Configuration Tools (Phase 4)
|
|
608
|
+
@mcp.tool()
|
|
609
|
+
async def create_network(
|
|
610
|
+
site_id: str,
|
|
611
|
+
name: str,
|
|
612
|
+
vlan_id: int,
|
|
613
|
+
subnet: str,
|
|
614
|
+
purpose: str = "corporate",
|
|
615
|
+
dhcp_enabled: bool = True,
|
|
616
|
+
dhcp_start: str | None = None,
|
|
617
|
+
dhcp_stop: str | None = None,
|
|
618
|
+
dhcp_dns_1: str | None = None,
|
|
619
|
+
dhcp_dns_2: str | None = None,
|
|
620
|
+
domain_name: str | None = None,
|
|
621
|
+
confirm: bool = False,
|
|
622
|
+
dry_run: bool = False,
|
|
623
|
+
) -> dict:
|
|
624
|
+
"""Create a new network/VLAN (requires confirm=True)."""
|
|
625
|
+
return await network_config_tools.create_network(
|
|
626
|
+
site_id,
|
|
627
|
+
name,
|
|
628
|
+
vlan_id,
|
|
629
|
+
subnet,
|
|
630
|
+
settings,
|
|
631
|
+
purpose,
|
|
632
|
+
dhcp_enabled,
|
|
633
|
+
dhcp_start,
|
|
634
|
+
dhcp_stop,
|
|
635
|
+
dhcp_dns_1,
|
|
636
|
+
dhcp_dns_2,
|
|
637
|
+
domain_name,
|
|
638
|
+
confirm,
|
|
639
|
+
dry_run,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@mcp.tool()
|
|
644
|
+
async def update_network(
|
|
645
|
+
site_id: str,
|
|
646
|
+
network_id: str,
|
|
647
|
+
name: str | None = None,
|
|
648
|
+
vlan_id: int | None = None,
|
|
649
|
+
subnet: str | None = None,
|
|
650
|
+
purpose: str | None = None,
|
|
651
|
+
dhcp_enabled: bool | None = None,
|
|
652
|
+
dhcp_start: str | None = None,
|
|
653
|
+
dhcp_stop: str | None = None,
|
|
654
|
+
dhcp_dns_1: str | None = None,
|
|
655
|
+
dhcp_dns_2: str | None = None,
|
|
656
|
+
domain_name: str | None = None,
|
|
657
|
+
confirm: bool = False,
|
|
658
|
+
dry_run: bool = False,
|
|
659
|
+
) -> dict:
|
|
660
|
+
"""Update an existing network (requires confirm=True)."""
|
|
661
|
+
return await network_config_tools.update_network(
|
|
662
|
+
site_id,
|
|
663
|
+
network_id,
|
|
664
|
+
settings,
|
|
665
|
+
name,
|
|
666
|
+
vlan_id,
|
|
667
|
+
subnet,
|
|
668
|
+
purpose,
|
|
669
|
+
dhcp_enabled,
|
|
670
|
+
dhcp_start,
|
|
671
|
+
dhcp_stop,
|
|
672
|
+
dhcp_dns_1,
|
|
673
|
+
dhcp_dns_2,
|
|
674
|
+
domain_name,
|
|
675
|
+
confirm,
|
|
676
|
+
dry_run,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@mcp.tool()
|
|
681
|
+
async def delete_network(
|
|
682
|
+
site_id: str, network_id: str, confirm: bool = False, dry_run: bool = False
|
|
683
|
+
) -> dict:
|
|
684
|
+
"""Delete a network (requires confirm=True)."""
|
|
685
|
+
return await network_config_tools.delete_network(
|
|
686
|
+
site_id, network_id, settings, confirm, dry_run
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# Device Control Tools (Phase 4)
|
|
691
|
+
@mcp.tool()
|
|
692
|
+
async def restart_device(
|
|
693
|
+
site_id: str, device_mac: str, confirm: bool = False, dry_run: bool = False
|
|
694
|
+
) -> dict:
|
|
695
|
+
"""Restart a UniFi device (requires confirm=True)."""
|
|
696
|
+
return await device_control_tools.restart_device(
|
|
697
|
+
site_id, device_mac, settings, confirm, dry_run
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@mcp.tool()
|
|
702
|
+
async def locate_device(
|
|
703
|
+
site_id: str,
|
|
704
|
+
device_mac: str,
|
|
705
|
+
enabled: bool = True,
|
|
706
|
+
confirm: bool = False,
|
|
707
|
+
dry_run: bool = False,
|
|
708
|
+
) -> dict:
|
|
709
|
+
"""Enable/disable LED locate mode on a device (requires confirm=True)."""
|
|
710
|
+
return await device_control_tools.locate_device(
|
|
711
|
+
site_id, device_mac, settings, enabled, confirm, dry_run
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
@mcp.tool()
|
|
716
|
+
async def upgrade_device(
|
|
717
|
+
site_id: str,
|
|
718
|
+
device_mac: str,
|
|
719
|
+
firmware_url: str | None = None,
|
|
720
|
+
confirm: bool = False,
|
|
721
|
+
dry_run: bool = False,
|
|
722
|
+
) -> dict:
|
|
723
|
+
"""Trigger firmware upgrade for a device (requires confirm=True)."""
|
|
724
|
+
return await device_control_tools.upgrade_device(
|
|
725
|
+
site_id, device_mac, settings, firmware_url, confirm, dry_run
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
# Client Management Tools (Phase 4)
|
|
730
|
+
@mcp.tool()
|
|
731
|
+
async def block_client(
|
|
732
|
+
site_id: str, client_mac: str, confirm: bool = False, dry_run: bool = False
|
|
733
|
+
) -> dict:
|
|
734
|
+
"""Block a client from accessing the network (requires confirm=True)."""
|
|
735
|
+
return await client_mgmt_tools.block_client(site_id, client_mac, settings, confirm, dry_run)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@mcp.tool()
|
|
739
|
+
async def unblock_client(
|
|
740
|
+
site_id: str, client_mac: str, confirm: bool = False, dry_run: bool = False
|
|
741
|
+
) -> dict:
|
|
742
|
+
"""Unblock a previously blocked client (requires confirm=True)."""
|
|
743
|
+
return await client_mgmt_tools.unblock_client(site_id, client_mac, settings, confirm, dry_run)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@mcp.tool()
|
|
747
|
+
async def reconnect_client(
|
|
748
|
+
site_id: str, client_mac: str, confirm: bool = False, dry_run: bool = False
|
|
749
|
+
) -> dict:
|
|
750
|
+
"""Force a client to reconnect (requires confirm=True)."""
|
|
751
|
+
return await client_mgmt_tools.reconnect_client(site_id, client_mac, settings, confirm, dry_run)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# WiFi Network (SSID) Management Tools (Phase 5)
|
|
755
|
+
@mcp.tool()
|
|
756
|
+
async def list_wlans(
|
|
757
|
+
site_id: str, limit: int | None = None, offset: int | None = None
|
|
758
|
+
) -> list[dict]:
|
|
759
|
+
"""List all wireless networks (SSIDs) in a site."""
|
|
760
|
+
return await wifi_tools.list_wlans(site_id, settings, limit, offset)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@mcp.tool()
|
|
764
|
+
async def create_wlan(
|
|
765
|
+
site_id: str,
|
|
766
|
+
name: str,
|
|
767
|
+
security: str,
|
|
768
|
+
password: str | None = None,
|
|
769
|
+
enabled: bool = True,
|
|
770
|
+
is_guest: bool = False,
|
|
771
|
+
wpa_mode: str = "wpa2",
|
|
772
|
+
wpa_enc: str = "ccmp",
|
|
773
|
+
vlan_id: int | None = None,
|
|
774
|
+
hide_ssid: bool = False,
|
|
775
|
+
confirm: bool = False,
|
|
776
|
+
dry_run: bool = False,
|
|
777
|
+
) -> dict:
|
|
778
|
+
"""Create a new wireless network/SSID (requires confirm=True)."""
|
|
779
|
+
return await wifi_tools.create_wlan(
|
|
780
|
+
site_id,
|
|
781
|
+
name,
|
|
782
|
+
security,
|
|
783
|
+
settings,
|
|
784
|
+
password,
|
|
785
|
+
enabled,
|
|
786
|
+
is_guest,
|
|
787
|
+
wpa_mode,
|
|
788
|
+
wpa_enc,
|
|
789
|
+
vlan_id,
|
|
790
|
+
hide_ssid,
|
|
791
|
+
confirm,
|
|
792
|
+
dry_run,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@mcp.tool()
|
|
797
|
+
async def update_wlan(
|
|
798
|
+
site_id: str,
|
|
799
|
+
wlan_id: str,
|
|
800
|
+
name: str | None = None,
|
|
801
|
+
security: str | None = None,
|
|
802
|
+
password: str | None = None,
|
|
803
|
+
enabled: bool | None = None,
|
|
804
|
+
is_guest: bool | None = None,
|
|
805
|
+
wpa_mode: str | None = None,
|
|
806
|
+
wpa_enc: str | None = None,
|
|
807
|
+
vlan_id: int | None = None,
|
|
808
|
+
hide_ssid: bool | None = None,
|
|
809
|
+
confirm: bool = False,
|
|
810
|
+
dry_run: bool = False,
|
|
811
|
+
) -> dict:
|
|
812
|
+
"""Update an existing wireless network (requires confirm=True)."""
|
|
813
|
+
return await wifi_tools.update_wlan(
|
|
814
|
+
site_id,
|
|
815
|
+
wlan_id,
|
|
816
|
+
settings,
|
|
817
|
+
name,
|
|
818
|
+
security,
|
|
819
|
+
password,
|
|
820
|
+
enabled,
|
|
821
|
+
is_guest,
|
|
822
|
+
wpa_mode,
|
|
823
|
+
wpa_enc,
|
|
824
|
+
vlan_id,
|
|
825
|
+
hide_ssid,
|
|
826
|
+
confirm,
|
|
827
|
+
dry_run,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@mcp.tool()
|
|
832
|
+
async def delete_wlan(
|
|
833
|
+
site_id: str, wlan_id: str, confirm: bool = False, dry_run: bool = False
|
|
834
|
+
) -> dict:
|
|
835
|
+
"""Delete a wireless network (requires confirm=True)."""
|
|
836
|
+
return await wifi_tools.delete_wlan(site_id, wlan_id, settings, confirm, dry_run)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@mcp.tool()
|
|
840
|
+
async def get_wlan_statistics(site_id: str, wlan_id: str | None = None) -> dict:
|
|
841
|
+
"""Get WiFi usage statistics for a site or specific WLAN."""
|
|
842
|
+
return await wifi_tools.get_wlan_statistics(site_id, settings, wlan_id)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
# Port Forwarding Management Tools (Phase 5)
|
|
846
|
+
@mcp.tool()
|
|
847
|
+
async def list_port_forwards(
|
|
848
|
+
site_id: str, limit: int | None = None, offset: int | None = None
|
|
849
|
+
) -> list[dict]:
|
|
850
|
+
"""List all port forwarding rules in a site."""
|
|
851
|
+
return await port_fwd_tools.list_port_forwards(site_id, settings, limit, offset)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@mcp.tool()
|
|
855
|
+
async def create_port_forward(
|
|
856
|
+
site_id: str,
|
|
857
|
+
name: str,
|
|
858
|
+
dst_port: int,
|
|
859
|
+
fwd_ip: str,
|
|
860
|
+
fwd_port: int,
|
|
861
|
+
protocol: str = "tcp_udp",
|
|
862
|
+
src: str = "any",
|
|
863
|
+
enabled: bool = True,
|
|
864
|
+
log: bool = False,
|
|
865
|
+
confirm: bool = False,
|
|
866
|
+
dry_run: bool = False,
|
|
867
|
+
) -> dict:
|
|
868
|
+
"""Create a port forwarding rule (requires confirm=True)."""
|
|
869
|
+
return await port_fwd_tools.create_port_forward(
|
|
870
|
+
site_id,
|
|
871
|
+
name,
|
|
872
|
+
dst_port,
|
|
873
|
+
fwd_ip,
|
|
874
|
+
fwd_port,
|
|
875
|
+
settings,
|
|
876
|
+
protocol,
|
|
877
|
+
src,
|
|
878
|
+
enabled,
|
|
879
|
+
log,
|
|
880
|
+
confirm,
|
|
881
|
+
dry_run,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@mcp.tool()
|
|
886
|
+
async def delete_port_forward(
|
|
887
|
+
site_id: str, rule_id: str, confirm: bool = False, dry_run: bool = False
|
|
888
|
+
) -> dict:
|
|
889
|
+
"""Delete a port forwarding rule (requires confirm=True)."""
|
|
890
|
+
return await port_fwd_tools.delete_port_forward(site_id, rule_id, settings, confirm, dry_run)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
# DPI Statistics Tools (Phase 5)
|
|
894
|
+
@mcp.tool()
|
|
895
|
+
async def get_dpi_statistics(site_id: str, time_range: str = "24h") -> dict:
|
|
896
|
+
"""Get Deep Packet Inspection statistics for a site."""
|
|
897
|
+
return await dpi_tools.get_dpi_statistics(site_id, settings, time_range)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@mcp.tool()
|
|
901
|
+
async def list_top_applications(
|
|
902
|
+
site_id: str, limit: int = 10, time_range: str = "24h"
|
|
903
|
+
) -> list[dict]:
|
|
904
|
+
"""List top applications by bandwidth usage."""
|
|
905
|
+
return await dpi_tools.list_top_applications(site_id, settings, limit, time_range)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@mcp.tool()
|
|
909
|
+
async def get_client_dpi(
|
|
910
|
+
site_id: str,
|
|
911
|
+
client_mac: str,
|
|
912
|
+
time_range: str = "24h",
|
|
913
|
+
limit: int | None = None,
|
|
914
|
+
offset: int | None = None,
|
|
915
|
+
) -> dict:
|
|
916
|
+
"""Get DPI statistics for a specific client."""
|
|
917
|
+
return await dpi_tools.get_client_dpi(site_id, client_mac, settings, time_range, limit, offset)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
# Application Information Tool
|
|
921
|
+
@mcp.tool()
|
|
922
|
+
async def get_application_info() -> dict:
|
|
923
|
+
"""Get UniFi Network application information."""
|
|
924
|
+
return await application_tools.get_application_info(settings)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
# Pending Devices and Adoption Tools
|
|
928
|
+
@mcp.tool()
|
|
929
|
+
async def list_pending_devices(
|
|
930
|
+
site_id: str, limit: int | None = None, offset: int | None = None
|
|
931
|
+
) -> list[dict]:
|
|
932
|
+
"""List devices awaiting adoption on the specified site."""
|
|
933
|
+
return await devices_tools.list_pending_devices(site_id, settings, limit, offset)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@mcp.tool()
|
|
937
|
+
async def adopt_device(
|
|
938
|
+
site_id: str,
|
|
939
|
+
device_id: str,
|
|
940
|
+
name: str | None = None,
|
|
941
|
+
confirm: bool = False,
|
|
942
|
+
dry_run: bool = False,
|
|
943
|
+
) -> dict:
|
|
944
|
+
"""Adopt a pending device onto the specified site (requires confirm=True)."""
|
|
945
|
+
return await devices_tools.adopt_device(site_id, device_id, settings, name, confirm, dry_run)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
@mcp.tool()
|
|
949
|
+
async def execute_port_action(
|
|
950
|
+
site_id: str,
|
|
951
|
+
device_id: str,
|
|
952
|
+
port_idx: int,
|
|
953
|
+
action: str,
|
|
954
|
+
params: dict | None = None,
|
|
955
|
+
confirm: bool = False,
|
|
956
|
+
dry_run: bool = False,
|
|
957
|
+
) -> dict:
|
|
958
|
+
"""Execute an action on a specific port (power-cycle, enable, disable) (requires confirm=True)."""
|
|
959
|
+
return await devices_tools.execute_port_action(
|
|
960
|
+
site_id, device_id, port_idx, action, settings, params, confirm, dry_run
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
# Enhanced Client Actions
|
|
965
|
+
@mcp.tool()
|
|
966
|
+
async def authorize_guest(
|
|
967
|
+
site_id: str,
|
|
968
|
+
client_mac: str,
|
|
969
|
+
duration: int,
|
|
970
|
+
upload_limit_kbps: int | None = None,
|
|
971
|
+
download_limit_kbps: int | None = None,
|
|
972
|
+
confirm: bool = False,
|
|
973
|
+
dry_run: bool = False,
|
|
974
|
+
) -> dict:
|
|
975
|
+
"""Authorize a guest client for network access (requires confirm=True)."""
|
|
976
|
+
return await client_mgmt_tools.authorize_guest(
|
|
977
|
+
site_id,
|
|
978
|
+
client_mac,
|
|
979
|
+
duration,
|
|
980
|
+
settings,
|
|
981
|
+
upload_limit_kbps,
|
|
982
|
+
download_limit_kbps,
|
|
983
|
+
confirm,
|
|
984
|
+
dry_run,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@mcp.tool()
|
|
989
|
+
async def limit_bandwidth(
|
|
990
|
+
site_id: str,
|
|
991
|
+
client_mac: str,
|
|
992
|
+
upload_limit_kbps: int | None = None,
|
|
993
|
+
download_limit_kbps: int | None = None,
|
|
994
|
+
confirm: bool = False,
|
|
995
|
+
dry_run: bool = False,
|
|
996
|
+
) -> dict:
|
|
997
|
+
"""Apply bandwidth restrictions to a client (requires confirm=True)."""
|
|
998
|
+
return await client_mgmt_tools.limit_bandwidth(
|
|
999
|
+
site_id, client_mac, settings, upload_limit_kbps, download_limit_kbps, confirm, dry_run
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
# Hotspot Voucher Tools
|
|
1004
|
+
@mcp.tool()
|
|
1005
|
+
async def list_vouchers(
|
|
1006
|
+
site_id: str,
|
|
1007
|
+
limit: int | None = None,
|
|
1008
|
+
offset: int | None = None,
|
|
1009
|
+
filter_expr: str | None = None,
|
|
1010
|
+
) -> list[dict]:
|
|
1011
|
+
"""List all hotspot vouchers for a site."""
|
|
1012
|
+
return await vouchers_tools.list_vouchers(site_id, settings, limit, offset, filter_expr)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@mcp.tool()
|
|
1016
|
+
async def get_voucher(site_id: str, voucher_id: str) -> dict:
|
|
1017
|
+
"""Get details for a specific voucher."""
|
|
1018
|
+
return await vouchers_tools.get_voucher(site_id, voucher_id, settings)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@mcp.tool()
|
|
1022
|
+
async def create_vouchers(
|
|
1023
|
+
site_id: str,
|
|
1024
|
+
count: int,
|
|
1025
|
+
duration: int,
|
|
1026
|
+
upload_limit_kbps: int | None = None,
|
|
1027
|
+
download_limit_kbps: int | None = None,
|
|
1028
|
+
upload_quota_mb: int | None = None,
|
|
1029
|
+
download_quota_mb: int | None = None,
|
|
1030
|
+
note: str | None = None,
|
|
1031
|
+
confirm: bool = False,
|
|
1032
|
+
dry_run: bool = False,
|
|
1033
|
+
) -> dict:
|
|
1034
|
+
"""Create new hotspot vouchers (requires confirm=True)."""
|
|
1035
|
+
return await vouchers_tools.create_vouchers(
|
|
1036
|
+
site_id,
|
|
1037
|
+
count,
|
|
1038
|
+
duration,
|
|
1039
|
+
settings,
|
|
1040
|
+
upload_limit_kbps,
|
|
1041
|
+
download_limit_kbps,
|
|
1042
|
+
upload_quota_mb,
|
|
1043
|
+
download_quota_mb,
|
|
1044
|
+
note,
|
|
1045
|
+
confirm,
|
|
1046
|
+
dry_run,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
@mcp.tool()
|
|
1051
|
+
async def delete_voucher(
|
|
1052
|
+
site_id: str, voucher_id: str, confirm: bool = False, dry_run: bool = False
|
|
1053
|
+
) -> dict:
|
|
1054
|
+
"""Delete a specific voucher (requires confirm=True)."""
|
|
1055
|
+
return await vouchers_tools.delete_voucher(site_id, voucher_id, settings, confirm, dry_run)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
@mcp.tool()
|
|
1059
|
+
async def bulk_delete_vouchers(
|
|
1060
|
+
site_id: str, filter_expr: str, confirm: bool = False, dry_run: bool = False
|
|
1061
|
+
) -> dict:
|
|
1062
|
+
"""Bulk delete vouchers using a filter expression (requires confirm=True)."""
|
|
1063
|
+
return await vouchers_tools.bulk_delete_vouchers(
|
|
1064
|
+
site_id, filter_expr, settings, confirm, dry_run
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
# RADIUS Profile Tools
|
|
1069
|
+
@mcp.tool()
|
|
1070
|
+
async def list_radius_profiles(site_id: str) -> list[dict]:
|
|
1071
|
+
"""List all RADIUS profiles for a site."""
|
|
1072
|
+
return await radius_tools.list_radius_profiles(site_id, settings)
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
@mcp.tool()
|
|
1076
|
+
async def get_radius_profile(site_id: str, profile_id: str) -> dict:
|
|
1077
|
+
"""Get details for a specific RADIUS profile."""
|
|
1078
|
+
return await radius_tools.get_radius_profile(site_id, profile_id, settings)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
@mcp.tool()
|
|
1082
|
+
async def create_radius_profile(
|
|
1083
|
+
site_id: str,
|
|
1084
|
+
name: str,
|
|
1085
|
+
auth_server: str,
|
|
1086
|
+
auth_secret: str,
|
|
1087
|
+
auth_port: int = 1812,
|
|
1088
|
+
acct_server: str | None = None,
|
|
1089
|
+
acct_port: int = 1813,
|
|
1090
|
+
acct_secret: str | None = None,
|
|
1091
|
+
use_same_secret: bool = True,
|
|
1092
|
+
vlan_enabled: bool = False,
|
|
1093
|
+
confirm: bool = False,
|
|
1094
|
+
dry_run: bool = False,
|
|
1095
|
+
) -> dict:
|
|
1096
|
+
"""Create a new RADIUS profile (requires confirm=True)."""
|
|
1097
|
+
return await radius_tools.create_radius_profile(
|
|
1098
|
+
site_id,
|
|
1099
|
+
name,
|
|
1100
|
+
auth_server,
|
|
1101
|
+
auth_secret,
|
|
1102
|
+
settings,
|
|
1103
|
+
auth_port,
|
|
1104
|
+
acct_server,
|
|
1105
|
+
acct_port,
|
|
1106
|
+
acct_secret,
|
|
1107
|
+
use_same_secret,
|
|
1108
|
+
vlan_enabled,
|
|
1109
|
+
confirm,
|
|
1110
|
+
dry_run,
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
@mcp.tool()
|
|
1115
|
+
async def update_radius_profile(
|
|
1116
|
+
site_id: str,
|
|
1117
|
+
profile_id: str,
|
|
1118
|
+
name: str | None = None,
|
|
1119
|
+
auth_server: str | None = None,
|
|
1120
|
+
auth_secret: str | None = None,
|
|
1121
|
+
auth_port: int | None = None,
|
|
1122
|
+
acct_server: str | None = None,
|
|
1123
|
+
acct_port: int | None = None,
|
|
1124
|
+
acct_secret: str | None = None,
|
|
1125
|
+
vlan_enabled: bool | None = None,
|
|
1126
|
+
enabled: bool | None = None,
|
|
1127
|
+
confirm: bool = False,
|
|
1128
|
+
dry_run: bool = False,
|
|
1129
|
+
) -> dict:
|
|
1130
|
+
"""Update an existing RADIUS profile (requires confirm=True)."""
|
|
1131
|
+
return await radius_tools.update_radius_profile(
|
|
1132
|
+
site_id,
|
|
1133
|
+
profile_id,
|
|
1134
|
+
settings,
|
|
1135
|
+
name,
|
|
1136
|
+
auth_server,
|
|
1137
|
+
auth_secret,
|
|
1138
|
+
auth_port,
|
|
1139
|
+
acct_server,
|
|
1140
|
+
acct_port,
|
|
1141
|
+
acct_secret,
|
|
1142
|
+
vlan_enabled,
|
|
1143
|
+
enabled,
|
|
1144
|
+
confirm,
|
|
1145
|
+
dry_run,
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
@mcp.tool()
|
|
1150
|
+
async def delete_radius_profile(
|
|
1151
|
+
site_id: str, profile_id: str, confirm: bool = False, dry_run: bool = False
|
|
1152
|
+
) -> dict:
|
|
1153
|
+
"""Delete a RADIUS profile (requires confirm=True)."""
|
|
1154
|
+
return await radius_tools.delete_radius_profile(site_id, profile_id, settings, confirm, dry_run)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
# RADIUS Account Tools
|
|
1158
|
+
@mcp.tool()
|
|
1159
|
+
async def list_radius_accounts(site_id: str) -> list[dict]:
|
|
1160
|
+
"""List all RADIUS accounts for a site."""
|
|
1161
|
+
return await radius_tools.list_radius_accounts(site_id, settings)
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
@mcp.tool()
|
|
1165
|
+
async def create_radius_account(
|
|
1166
|
+
site_id: str,
|
|
1167
|
+
username: str,
|
|
1168
|
+
password: str,
|
|
1169
|
+
vlan_id: int | None = None,
|
|
1170
|
+
enabled: bool = True,
|
|
1171
|
+
note: str | None = None,
|
|
1172
|
+
confirm: bool = False,
|
|
1173
|
+
dry_run: bool = False,
|
|
1174
|
+
) -> dict:
|
|
1175
|
+
"""Create a new RADIUS account (requires confirm=True)."""
|
|
1176
|
+
return await radius_tools.create_radius_account(
|
|
1177
|
+
site_id, username, password, settings, vlan_id, enabled, note, confirm, dry_run
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
@mcp.tool()
|
|
1182
|
+
async def delete_radius_account(
|
|
1183
|
+
site_id: str, account_id: str, confirm: bool = False, dry_run: bool = False
|
|
1184
|
+
) -> dict:
|
|
1185
|
+
"""Delete a RADIUS account (requires confirm=True)."""
|
|
1186
|
+
return await radius_tools.delete_radius_account(site_id, account_id, settings, confirm, dry_run)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
# Guest Portal Tools
|
|
1190
|
+
@mcp.tool()
|
|
1191
|
+
async def get_guest_portal_config(site_id: str) -> dict:
|
|
1192
|
+
"""Get guest portal configuration for a site."""
|
|
1193
|
+
return await radius_tools.get_guest_portal_config(site_id, settings)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
@mcp.tool()
|
|
1197
|
+
async def configure_guest_portal(
|
|
1198
|
+
site_id: str,
|
|
1199
|
+
portal_title: str | None = None,
|
|
1200
|
+
auth_method: str | None = None,
|
|
1201
|
+
password: str | None = None,
|
|
1202
|
+
session_timeout: int | None = None,
|
|
1203
|
+
redirect_enabled: bool | None = None,
|
|
1204
|
+
redirect_url: str | None = None,
|
|
1205
|
+
terms_of_service_enabled: bool | None = None,
|
|
1206
|
+
terms_of_service_text: str | None = None,
|
|
1207
|
+
confirm: bool = False,
|
|
1208
|
+
dry_run: bool = False,
|
|
1209
|
+
) -> dict:
|
|
1210
|
+
"""Configure guest portal settings (requires confirm=True)."""
|
|
1211
|
+
return await radius_tools.configure_guest_portal(
|
|
1212
|
+
site_id,
|
|
1213
|
+
settings,
|
|
1214
|
+
portal_title,
|
|
1215
|
+
auth_method,
|
|
1216
|
+
password,
|
|
1217
|
+
session_timeout,
|
|
1218
|
+
redirect_enabled,
|
|
1219
|
+
redirect_url,
|
|
1220
|
+
terms_of_service_enabled,
|
|
1221
|
+
terms_of_service_text,
|
|
1222
|
+
confirm,
|
|
1223
|
+
dry_run,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
# Hotspot Package Tools
|
|
1228
|
+
@mcp.tool()
|
|
1229
|
+
async def list_hotspot_packages(site_id: str) -> list[dict]:
|
|
1230
|
+
"""List all hotspot packages for a site."""
|
|
1231
|
+
return await radius_tools.list_hotspot_packages(site_id, settings)
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
@mcp.tool()
|
|
1235
|
+
async def create_hotspot_package(
|
|
1236
|
+
site_id: str,
|
|
1237
|
+
name: str,
|
|
1238
|
+
duration_minutes: int,
|
|
1239
|
+
download_limit_kbps: int | None = None,
|
|
1240
|
+
upload_limit_kbps: int | None = None,
|
|
1241
|
+
download_quota_mb: int | None = None,
|
|
1242
|
+
upload_quota_mb: int | None = None,
|
|
1243
|
+
price: float | None = None,
|
|
1244
|
+
currency: str = "USD",
|
|
1245
|
+
confirm: bool = False,
|
|
1246
|
+
dry_run: bool = False,
|
|
1247
|
+
) -> dict:
|
|
1248
|
+
"""Create a new hotspot package (requires confirm=True)."""
|
|
1249
|
+
return await radius_tools.create_hotspot_package(
|
|
1250
|
+
site_id,
|
|
1251
|
+
name,
|
|
1252
|
+
duration_minutes,
|
|
1253
|
+
settings,
|
|
1254
|
+
download_limit_kbps,
|
|
1255
|
+
upload_limit_kbps,
|
|
1256
|
+
download_quota_mb,
|
|
1257
|
+
upload_quota_mb,
|
|
1258
|
+
price,
|
|
1259
|
+
currency,
|
|
1260
|
+
confirm,
|
|
1261
|
+
dry_run,
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
@mcp.tool()
|
|
1266
|
+
async def delete_hotspot_package(
|
|
1267
|
+
site_id: str, package_id: str, confirm: bool = False, dry_run: bool = False
|
|
1268
|
+
) -> dict:
|
|
1269
|
+
"""Delete a hotspot package (requires confirm=True)."""
|
|
1270
|
+
return await radius_tools.delete_hotspot_package(
|
|
1271
|
+
site_id, package_id, settings, confirm, dry_run
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
# Firewall Zone Tools
|
|
1276
|
+
@mcp.tool()
|
|
1277
|
+
async def list_firewall_zones(site_id: str) -> list[dict]:
|
|
1278
|
+
"""List all firewall zones for a site."""
|
|
1279
|
+
return await firewall_zones_tools.list_firewall_zones(site_id, settings)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
@mcp.tool()
|
|
1283
|
+
async def create_firewall_zone(
|
|
1284
|
+
site_id: str,
|
|
1285
|
+
name: str,
|
|
1286
|
+
description: str | None = None,
|
|
1287
|
+
network_ids: list[str] | None = None,
|
|
1288
|
+
confirm: bool = False,
|
|
1289
|
+
dry_run: bool = False,
|
|
1290
|
+
) -> dict:
|
|
1291
|
+
"""Create a new firewall zone (requires confirm=True)."""
|
|
1292
|
+
return await firewall_zones_tools.create_firewall_zone(
|
|
1293
|
+
site_id, name, settings, description, network_ids, confirm, dry_run
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
@mcp.tool()
|
|
1298
|
+
async def update_firewall_zone(
|
|
1299
|
+
site_id: str,
|
|
1300
|
+
firewall_zone_id: str,
|
|
1301
|
+
name: str | None = None,
|
|
1302
|
+
description: str | None = None,
|
|
1303
|
+
network_ids: list[str] | None = None,
|
|
1304
|
+
confirm: bool = False,
|
|
1305
|
+
dry_run: bool = False,
|
|
1306
|
+
) -> dict:
|
|
1307
|
+
"""Update an existing firewall zone (requires confirm=True)."""
|
|
1308
|
+
return await firewall_zones_tools.update_firewall_zone(
|
|
1309
|
+
site_id, firewall_zone_id, settings, name, description, network_ids, confirm, dry_run
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
# QoS Profile Management Tools
|
|
1314
|
+
@mcp.tool()
|
|
1315
|
+
async def list_qos_profiles(
|
|
1316
|
+
site_id: str,
|
|
1317
|
+
limit: int = 100,
|
|
1318
|
+
offset: int = 0,
|
|
1319
|
+
) -> list[dict]:
|
|
1320
|
+
"""List all QoS profiles for traffic prioritization and shaping."""
|
|
1321
|
+
return await qos_tools.list_qos_profiles(site_id, settings, limit, offset)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
@mcp.tool()
|
|
1325
|
+
async def get_qos_profile(site_id: str, profile_id: str) -> dict:
|
|
1326
|
+
"""Get details for a specific QoS profile."""
|
|
1327
|
+
return await qos_tools.get_qos_profile(site_id, profile_id, settings)
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
@mcp.tool()
|
|
1331
|
+
async def create_qos_profile(
|
|
1332
|
+
site_id: str,
|
|
1333
|
+
name: str,
|
|
1334
|
+
priority_level: int,
|
|
1335
|
+
description: str | None = None,
|
|
1336
|
+
dscp_marking: int | None = None,
|
|
1337
|
+
bandwidth_limit_down_kbps: int | None = None,
|
|
1338
|
+
bandwidth_limit_up_kbps: int | None = None,
|
|
1339
|
+
bandwidth_guaranteed_down_kbps: int | None = None,
|
|
1340
|
+
bandwidth_guaranteed_up_kbps: int | None = None,
|
|
1341
|
+
ports: list[int] | None = None,
|
|
1342
|
+
protocols: list[str] | None = None,
|
|
1343
|
+
applications: list[str] | None = None,
|
|
1344
|
+
categories: list[str] | None = None,
|
|
1345
|
+
schedule_enabled: bool = False,
|
|
1346
|
+
schedule_days: list[str] | None = None,
|
|
1347
|
+
schedule_time_start: str | None = None,
|
|
1348
|
+
schedule_time_end: str | None = None,
|
|
1349
|
+
enabled: bool = True,
|
|
1350
|
+
confirm: bool = False,
|
|
1351
|
+
dry_run: bool = False,
|
|
1352
|
+
) -> dict:
|
|
1353
|
+
"""Create a new QoS profile with comprehensive traffic shaping (requires confirm=True)."""
|
|
1354
|
+
return await qos_tools.create_qos_profile(
|
|
1355
|
+
site_id,
|
|
1356
|
+
name,
|
|
1357
|
+
priority_level,
|
|
1358
|
+
settings,
|
|
1359
|
+
description,
|
|
1360
|
+
dscp_marking,
|
|
1361
|
+
bandwidth_limit_down_kbps,
|
|
1362
|
+
bandwidth_limit_up_kbps,
|
|
1363
|
+
bandwidth_guaranteed_down_kbps,
|
|
1364
|
+
bandwidth_guaranteed_up_kbps,
|
|
1365
|
+
ports,
|
|
1366
|
+
protocols,
|
|
1367
|
+
applications,
|
|
1368
|
+
categories,
|
|
1369
|
+
schedule_enabled,
|
|
1370
|
+
schedule_days,
|
|
1371
|
+
schedule_time_start,
|
|
1372
|
+
schedule_time_end,
|
|
1373
|
+
enabled,
|
|
1374
|
+
confirm,
|
|
1375
|
+
dry_run,
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
@mcp.tool()
|
|
1380
|
+
async def update_qos_profile(
|
|
1381
|
+
site_id: str,
|
|
1382
|
+
profile_id: str,
|
|
1383
|
+
name: str | None = None,
|
|
1384
|
+
priority_level: int | None = None,
|
|
1385
|
+
description: str | None = None,
|
|
1386
|
+
dscp_marking: int | None = None,
|
|
1387
|
+
bandwidth_limit_down_kbps: int | None = None,
|
|
1388
|
+
bandwidth_limit_up_kbps: int | None = None,
|
|
1389
|
+
bandwidth_guaranteed_down_kbps: int | None = None,
|
|
1390
|
+
bandwidth_guaranteed_up_kbps: int | None = None,
|
|
1391
|
+
enabled: bool | None = None,
|
|
1392
|
+
confirm: bool = False,
|
|
1393
|
+
dry_run: bool = False,
|
|
1394
|
+
) -> dict:
|
|
1395
|
+
"""Update an existing QoS profile (requires confirm=True)."""
|
|
1396
|
+
return await qos_tools.update_qos_profile(
|
|
1397
|
+
site_id,
|
|
1398
|
+
profile_id,
|
|
1399
|
+
settings,
|
|
1400
|
+
name,
|
|
1401
|
+
priority_level,
|
|
1402
|
+
description,
|
|
1403
|
+
dscp_marking,
|
|
1404
|
+
bandwidth_limit_down_kbps,
|
|
1405
|
+
bandwidth_limit_up_kbps,
|
|
1406
|
+
bandwidth_guaranteed_down_kbps,
|
|
1407
|
+
bandwidth_guaranteed_up_kbps,
|
|
1408
|
+
enabled,
|
|
1409
|
+
confirm,
|
|
1410
|
+
dry_run,
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
@mcp.tool()
|
|
1415
|
+
async def delete_qos_profile(site_id: str, profile_id: str, confirm: bool = False) -> dict:
|
|
1416
|
+
"""Delete a QoS profile (requires confirm=True)."""
|
|
1417
|
+
return await qos_tools.delete_qos_profile(site_id, profile_id, settings, confirm)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
# ProAV Profile Management Tools
|
|
1421
|
+
@mcp.tool()
|
|
1422
|
+
async def list_proav_templates() -> list[dict]:
|
|
1423
|
+
"""List available ProAV protocol templates (Dante, Q-SYS, SDVoE, AVB, RAVENNA, NDI, SMPTE 2110) and reference profiles."""
|
|
1424
|
+
return await qos_tools.list_proav_templates(settings)
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
@mcp.tool()
|
|
1428
|
+
async def create_proav_profile(
|
|
1429
|
+
site_id: str,
|
|
1430
|
+
protocol: str,
|
|
1431
|
+
name: str | None = None,
|
|
1432
|
+
customize_ports: list[int] | None = None,
|
|
1433
|
+
customize_bandwidth_down_kbps: int | None = None,
|
|
1434
|
+
customize_bandwidth_up_kbps: int | None = None,
|
|
1435
|
+
customize_dscp: int | None = None,
|
|
1436
|
+
enabled: bool = True,
|
|
1437
|
+
confirm: bool = False,
|
|
1438
|
+
dry_run: bool = False,
|
|
1439
|
+
) -> dict:
|
|
1440
|
+
"""Create a QoS profile from a ProAV or reference template (requires confirm=True)."""
|
|
1441
|
+
return await qos_tools.create_proav_profile(
|
|
1442
|
+
site_id,
|
|
1443
|
+
protocol,
|
|
1444
|
+
settings,
|
|
1445
|
+
name,
|
|
1446
|
+
customize_ports,
|
|
1447
|
+
customize_bandwidth_down_kbps,
|
|
1448
|
+
customize_bandwidth_up_kbps,
|
|
1449
|
+
customize_dscp,
|
|
1450
|
+
enabled,
|
|
1451
|
+
confirm,
|
|
1452
|
+
dry_run,
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
@mcp.tool()
|
|
1457
|
+
async def validate_proav_profile(protocol: str, bandwidth_mbps: int | None = None) -> dict:
|
|
1458
|
+
"""Validate ProAV profile requirements and provide recommendations."""
|
|
1459
|
+
return await qos_tools.validate_proav_profile(protocol, settings, bandwidth_mbps)
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
# Smart Queue Management Tools
|
|
1463
|
+
@mcp.tool()
|
|
1464
|
+
async def get_smart_queue_config(site_id: str) -> dict:
|
|
1465
|
+
"""Get Smart Queue Management (SQM) configuration for bufferbloat mitigation."""
|
|
1466
|
+
return await qos_tools.get_smart_queue_config(site_id, settings)
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
@mcp.tool()
|
|
1470
|
+
async def configure_smart_queue(
|
|
1471
|
+
site_id: str,
|
|
1472
|
+
wan_id: str,
|
|
1473
|
+
download_kbps: int,
|
|
1474
|
+
upload_kbps: int,
|
|
1475
|
+
algorithm: str = "fq_codel",
|
|
1476
|
+
overhead_bytes: int = 44,
|
|
1477
|
+
confirm: bool = False,
|
|
1478
|
+
dry_run: bool = False,
|
|
1479
|
+
) -> dict:
|
|
1480
|
+
"""Configure Smart Queue Management (SQM) for bufferbloat mitigation (requires confirm=True)."""
|
|
1481
|
+
return await qos_tools.configure_smart_queue(
|
|
1482
|
+
site_id,
|
|
1483
|
+
wan_id,
|
|
1484
|
+
download_kbps,
|
|
1485
|
+
upload_kbps,
|
|
1486
|
+
settings,
|
|
1487
|
+
algorithm,
|
|
1488
|
+
overhead_bytes,
|
|
1489
|
+
confirm,
|
|
1490
|
+
dry_run,
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
@mcp.tool()
|
|
1495
|
+
async def disable_smart_queue(site_id: str, wan_id: str, confirm: bool = False) -> dict:
|
|
1496
|
+
"""Disable Smart Queue Management (SQM) (requires confirm=True)."""
|
|
1497
|
+
return await qos_tools.disable_smart_queue(site_id, wan_id, settings, confirm)
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
# Traffic Route Management Tools
|
|
1501
|
+
@mcp.tool()
|
|
1502
|
+
async def list_traffic_routes(
|
|
1503
|
+
site_id: str,
|
|
1504
|
+
limit: int = 100,
|
|
1505
|
+
offset: int = 0,
|
|
1506
|
+
) -> list[dict]:
|
|
1507
|
+
"""List all policy-based traffic routing rules."""
|
|
1508
|
+
return await qos_tools.list_traffic_routes(site_id, settings, limit, offset)
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
@mcp.tool()
|
|
1512
|
+
async def create_traffic_route(
|
|
1513
|
+
site_id: str,
|
|
1514
|
+
name: str,
|
|
1515
|
+
action: str,
|
|
1516
|
+
description: str | None = None,
|
|
1517
|
+
source_ip: str | None = None,
|
|
1518
|
+
destination_ip: str | None = None,
|
|
1519
|
+
source_port: int | None = None,
|
|
1520
|
+
destination_port: int | None = None,
|
|
1521
|
+
protocol: str | None = None,
|
|
1522
|
+
vlan_id: int | None = None,
|
|
1523
|
+
dscp_marking: int | None = None,
|
|
1524
|
+
bandwidth_limit_kbps: int | None = None,
|
|
1525
|
+
priority: int = 100,
|
|
1526
|
+
enabled: bool = True,
|
|
1527
|
+
confirm: bool = False,
|
|
1528
|
+
dry_run: bool = False,
|
|
1529
|
+
) -> dict:
|
|
1530
|
+
"""Create a new policy-based traffic routing rule (requires confirm=True)."""
|
|
1531
|
+
return await qos_tools.create_traffic_route(
|
|
1532
|
+
site_id,
|
|
1533
|
+
name,
|
|
1534
|
+
action,
|
|
1535
|
+
settings,
|
|
1536
|
+
description,
|
|
1537
|
+
source_ip,
|
|
1538
|
+
destination_ip,
|
|
1539
|
+
source_port,
|
|
1540
|
+
destination_port,
|
|
1541
|
+
protocol,
|
|
1542
|
+
vlan_id,
|
|
1543
|
+
dscp_marking,
|
|
1544
|
+
bandwidth_limit_kbps,
|
|
1545
|
+
priority,
|
|
1546
|
+
enabled,
|
|
1547
|
+
confirm,
|
|
1548
|
+
dry_run,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
@mcp.tool()
|
|
1553
|
+
async def update_traffic_route(
|
|
1554
|
+
site_id: str,
|
|
1555
|
+
route_id: str,
|
|
1556
|
+
name: str | None = None,
|
|
1557
|
+
action: str | None = None,
|
|
1558
|
+
description: str | None = None,
|
|
1559
|
+
enabled: bool | None = None,
|
|
1560
|
+
priority: int | None = None,
|
|
1561
|
+
confirm: bool = False,
|
|
1562
|
+
dry_run: bool = False,
|
|
1563
|
+
) -> dict:
|
|
1564
|
+
"""Update an existing traffic routing rule (requires confirm=True)."""
|
|
1565
|
+
return await qos_tools.update_traffic_route(
|
|
1566
|
+
site_id,
|
|
1567
|
+
route_id,
|
|
1568
|
+
settings,
|
|
1569
|
+
name,
|
|
1570
|
+
action,
|
|
1571
|
+
description,
|
|
1572
|
+
enabled,
|
|
1573
|
+
priority,
|
|
1574
|
+
confirm,
|
|
1575
|
+
dry_run,
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
@mcp.tool()
|
|
1580
|
+
async def delete_traffic_route(site_id: str, route_id: str, confirm: bool = False) -> dict:
|
|
1581
|
+
"""Delete a traffic routing rule (requires confirm=True)."""
|
|
1582
|
+
return await qos_tools.delete_traffic_route(site_id, route_id, settings, confirm)
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
# ACL Tools
|
|
1586
|
+
@mcp.tool()
|
|
1587
|
+
async def list_acl_rules(
|
|
1588
|
+
site_id: str,
|
|
1589
|
+
limit: int | None = None,
|
|
1590
|
+
offset: int | None = None,
|
|
1591
|
+
filter_expr: str | None = None,
|
|
1592
|
+
) -> list[dict]:
|
|
1593
|
+
"""List all ACL rules for a site."""
|
|
1594
|
+
return await acls_tools.list_acl_rules(site_id, settings, limit, offset, filter_expr)
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
@mcp.tool()
|
|
1598
|
+
async def get_acl_rule(site_id: str, acl_rule_id: str) -> dict:
|
|
1599
|
+
"""Get details for a specific ACL rule."""
|
|
1600
|
+
return await acls_tools.get_acl_rule(site_id, acl_rule_id, settings)
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
@mcp.tool()
|
|
1604
|
+
async def create_acl_rule(
|
|
1605
|
+
site_id: str,
|
|
1606
|
+
name: str,
|
|
1607
|
+
action: str,
|
|
1608
|
+
enabled: bool = True,
|
|
1609
|
+
source_type: str | None = None,
|
|
1610
|
+
source_id: str | None = None,
|
|
1611
|
+
source_network: str | None = None,
|
|
1612
|
+
destination_type: str | None = None,
|
|
1613
|
+
destination_id: str | None = None,
|
|
1614
|
+
destination_network: str | None = None,
|
|
1615
|
+
protocol: str | None = None,
|
|
1616
|
+
src_port: int | None = None,
|
|
1617
|
+
dst_port: int | None = None,
|
|
1618
|
+
priority: int = 100,
|
|
1619
|
+
description: str | None = None,
|
|
1620
|
+
confirm: bool = False,
|
|
1621
|
+
dry_run: bool = False,
|
|
1622
|
+
) -> dict:
|
|
1623
|
+
"""Create a new ACL rule (requires confirm=True)."""
|
|
1624
|
+
return await acls_tools.create_acl_rule(
|
|
1625
|
+
site_id,
|
|
1626
|
+
name,
|
|
1627
|
+
action,
|
|
1628
|
+
settings,
|
|
1629
|
+
enabled,
|
|
1630
|
+
source_type,
|
|
1631
|
+
source_id,
|
|
1632
|
+
source_network,
|
|
1633
|
+
destination_type,
|
|
1634
|
+
destination_id,
|
|
1635
|
+
destination_network,
|
|
1636
|
+
protocol,
|
|
1637
|
+
src_port,
|
|
1638
|
+
dst_port,
|
|
1639
|
+
priority,
|
|
1640
|
+
description,
|
|
1641
|
+
confirm,
|
|
1642
|
+
dry_run,
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
@mcp.tool()
|
|
1647
|
+
async def update_acl_rule(
|
|
1648
|
+
site_id: str,
|
|
1649
|
+
acl_rule_id: str,
|
|
1650
|
+
name: str | None = None,
|
|
1651
|
+
action: str | None = None,
|
|
1652
|
+
enabled: bool | None = None,
|
|
1653
|
+
source_type: str | None = None,
|
|
1654
|
+
source_id: str | None = None,
|
|
1655
|
+
source_network: str | None = None,
|
|
1656
|
+
destination_type: str | None = None,
|
|
1657
|
+
destination_id: str | None = None,
|
|
1658
|
+
destination_network: str | None = None,
|
|
1659
|
+
protocol: str | None = None,
|
|
1660
|
+
src_port: int | None = None,
|
|
1661
|
+
dst_port: int | None = None,
|
|
1662
|
+
priority: int | None = None,
|
|
1663
|
+
description: str | None = None,
|
|
1664
|
+
confirm: bool = False,
|
|
1665
|
+
dry_run: bool = False,
|
|
1666
|
+
) -> dict:
|
|
1667
|
+
"""Update an existing ACL rule (requires confirm=True)."""
|
|
1668
|
+
return await acls_tools.update_acl_rule(
|
|
1669
|
+
site_id,
|
|
1670
|
+
acl_rule_id,
|
|
1671
|
+
settings,
|
|
1672
|
+
name,
|
|
1673
|
+
action,
|
|
1674
|
+
enabled,
|
|
1675
|
+
source_type,
|
|
1676
|
+
source_id,
|
|
1677
|
+
source_network,
|
|
1678
|
+
destination_type,
|
|
1679
|
+
destination_id,
|
|
1680
|
+
destination_network,
|
|
1681
|
+
protocol,
|
|
1682
|
+
src_port,
|
|
1683
|
+
dst_port,
|
|
1684
|
+
priority,
|
|
1685
|
+
description,
|
|
1686
|
+
confirm,
|
|
1687
|
+
dry_run,
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
@mcp.tool()
|
|
1692
|
+
async def delete_acl_rule(
|
|
1693
|
+
site_id: str, acl_rule_id: str, confirm: bool = False, dry_run: bool = False
|
|
1694
|
+
) -> dict:
|
|
1695
|
+
"""Delete an ACL rule (requires confirm=True)."""
|
|
1696
|
+
return await acls_tools.delete_acl_rule(site_id, acl_rule_id, settings, confirm, dry_run)
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
# WAN Connections Tool
|
|
1700
|
+
@mcp.tool()
|
|
1701
|
+
async def list_wan_connections(site_id: str) -> list[dict]:
|
|
1702
|
+
"""List all WAN connections for a site."""
|
|
1703
|
+
return await wans_tools.list_wan_connections(site_id, settings)
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
# DPI and Country Tools
|
|
1707
|
+
@mcp.tool()
|
|
1708
|
+
async def list_dpi_categories() -> list[dict]:
|
|
1709
|
+
"""List all DPI categories."""
|
|
1710
|
+
return await dpi_new_tools.list_dpi_categories(settings)
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
@mcp.tool()
|
|
1714
|
+
async def list_dpi_applications(
|
|
1715
|
+
limit: int | None = None,
|
|
1716
|
+
offset: int | None = None,
|
|
1717
|
+
filter_expr: str | None = None,
|
|
1718
|
+
) -> list[dict]:
|
|
1719
|
+
"""List all DPI applications."""
|
|
1720
|
+
return await dpi_new_tools.list_dpi_applications(settings, limit, offset, filter_expr)
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
@mcp.tool()
|
|
1724
|
+
async def list_countries(
|
|
1725
|
+
limit: int | None = None,
|
|
1726
|
+
offset: int | None = None,
|
|
1727
|
+
) -> list[dict]:
|
|
1728
|
+
"""List all countries with ISO codes (read-only)."""
|
|
1729
|
+
return await ref_tools.list_countries(settings, limit, offset)
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
# Zone-Based Firewall Matrix Tools
|
|
1733
|
+
# ⚠️ REMOVED: All zone policy matrix and application blocking tools have been removed
|
|
1734
|
+
# because the UniFi API endpoints do not exist (verified on API v10.0.156).
|
|
1735
|
+
# See tests/verification/PHASE2_FINDINGS.md for details.
|
|
1736
|
+
#
|
|
1737
|
+
# Removed tools:
|
|
1738
|
+
# - get_zbf_matrix (endpoint /firewall/policies/zone-matrix does not exist)
|
|
1739
|
+
# - get_zone_policies (endpoint /firewall/policies/zones/{id} does not exist)
|
|
1740
|
+
# - update_zbf_policy (endpoint /firewall/policies/zone-matrix/{src}/{dst} does not exist)
|
|
1741
|
+
# - block_application_by_zone (endpoint /firewall/zones/{id}/app-block does not exist)
|
|
1742
|
+
# - list_blocked_applications (endpoint /firewall/zones/{id}/app-block does not exist)
|
|
1743
|
+
# - get_zone_matrix_policy (endpoint /firewall/policies/zone-matrix/{src}/{dst} does not exist)
|
|
1744
|
+
# - delete_zbf_policy (endpoint /firewall/policies/zone-matrix/{src}/{dst} does not exist)
|
|
1745
|
+
#
|
|
1746
|
+
# Alternative: Configure zone policies manually in UniFi Console UI
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
@mcp.tool()
|
|
1750
|
+
async def assign_network_to_zone(
|
|
1751
|
+
site_id: str,
|
|
1752
|
+
zone_id: str,
|
|
1753
|
+
network_id: str,
|
|
1754
|
+
confirm: bool = False,
|
|
1755
|
+
dry_run: bool = False,
|
|
1756
|
+
) -> dict:
|
|
1757
|
+
"""Dynamically assign a network to a zone (requires confirm=True)."""
|
|
1758
|
+
return await firewall_zones_tools.assign_network_to_zone(
|
|
1759
|
+
site_id, zone_id, network_id, settings, confirm, dry_run
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
@mcp.tool()
|
|
1764
|
+
async def get_zone_networks(site_id: str, zone_id: str) -> list[dict]:
|
|
1765
|
+
"""List all networks in a zone."""
|
|
1766
|
+
return await firewall_zones_tools.get_zone_networks(site_id, zone_id, settings)
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
@mcp.tool()
|
|
1770
|
+
async def delete_firewall_zone(
|
|
1771
|
+
site_id: str,
|
|
1772
|
+
zone_id: str,
|
|
1773
|
+
confirm: bool = False,
|
|
1774
|
+
dry_run: bool = False,
|
|
1775
|
+
) -> dict:
|
|
1776
|
+
"""Delete a firewall zone (requires confirm=True)."""
|
|
1777
|
+
return await firewall_zones_tools.delete_firewall_zone(
|
|
1778
|
+
site_id, zone_id, settings, confirm, dry_run
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
@mcp.tool()
|
|
1783
|
+
async def unassign_network_from_zone(
|
|
1784
|
+
site_id: str,
|
|
1785
|
+
zone_id: str,
|
|
1786
|
+
network_id: str,
|
|
1787
|
+
confirm: bool = False,
|
|
1788
|
+
dry_run: bool = False,
|
|
1789
|
+
) -> dict:
|
|
1790
|
+
"""Remove a network from a firewall zone (requires confirm=True)."""
|
|
1791
|
+
return await firewall_zones_tools.unassign_network_from_zone(
|
|
1792
|
+
site_id, zone_id, network_id, settings, confirm, dry_run
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
# ⚠️ REMOVED: get_zone_statistics - endpoint does not exist
|
|
1797
|
+
# Zone statistics endpoint (/firewall/zones/{id}/statistics) does not exist in UniFi API v10.0.156.
|
|
1798
|
+
# Monitor traffic via /sites/{siteId}/clients endpoint instead.
|
|
1799
|
+
|
|
1800
|
+
# ⚠️ REMOVED: get_zone_matrix_policy - endpoint does not exist
|
|
1801
|
+
# Zone matrix policy endpoint does not exist in UniFi API v10.0.156.
|
|
1802
|
+
|
|
1803
|
+
# ⚠️ REMOVED: delete_zbf_policy - endpoint does not exist
|
|
1804
|
+
# Zone policy delete endpoint does not exist in UniFi API v10.0.156.
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
# Traffic Flows Tools
|
|
1808
|
+
@mcp.tool()
|
|
1809
|
+
async def get_traffic_flows(
|
|
1810
|
+
site_id: str,
|
|
1811
|
+
source_ip: str | None = None,
|
|
1812
|
+
destination_ip: str | None = None,
|
|
1813
|
+
protocol: str | None = None,
|
|
1814
|
+
application_id: str | None = None,
|
|
1815
|
+
time_range: str = "24h",
|
|
1816
|
+
limit: int | None = None,
|
|
1817
|
+
offset: int | None = None,
|
|
1818
|
+
) -> list[dict]:
|
|
1819
|
+
"""Retrieve real-time traffic flows."""
|
|
1820
|
+
return await traffic_flows_tools.get_traffic_flows(
|
|
1821
|
+
site_id,
|
|
1822
|
+
settings,
|
|
1823
|
+
source_ip,
|
|
1824
|
+
destination_ip,
|
|
1825
|
+
protocol,
|
|
1826
|
+
application_id,
|
|
1827
|
+
time_range,
|
|
1828
|
+
limit,
|
|
1829
|
+
offset,
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
@mcp.tool()
|
|
1834
|
+
async def get_flow_statistics(site_id: str, time_range: str = "24h") -> dict:
|
|
1835
|
+
"""Get aggregate flow statistics."""
|
|
1836
|
+
return await traffic_flows_tools.get_flow_statistics(site_id, settings, time_range)
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
@mcp.tool()
|
|
1840
|
+
async def get_traffic_flow_details(site_id: str, flow_id: str) -> dict:
|
|
1841
|
+
"""Get details for a specific traffic flow."""
|
|
1842
|
+
return await traffic_flows_tools.get_traffic_flow_details(site_id, flow_id, settings)
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
@mcp.tool()
|
|
1846
|
+
async def get_top_flows(
|
|
1847
|
+
site_id: str,
|
|
1848
|
+
limit: int = 10,
|
|
1849
|
+
time_range: str = "24h",
|
|
1850
|
+
sort_by: str = "bytes",
|
|
1851
|
+
) -> list[dict]:
|
|
1852
|
+
"""Get top bandwidth-consuming flows."""
|
|
1853
|
+
return await traffic_flows_tools.get_top_flows(site_id, settings, limit, time_range, sort_by)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
@mcp.tool()
|
|
1857
|
+
async def get_flow_risks(
|
|
1858
|
+
site_id: str,
|
|
1859
|
+
time_range: str = "24h",
|
|
1860
|
+
min_risk_level: str | None = None,
|
|
1861
|
+
) -> list[dict]:
|
|
1862
|
+
"""Get risk assessment for flows."""
|
|
1863
|
+
return await traffic_flows_tools.get_flow_risks(site_id, settings, time_range, min_risk_level)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
@mcp.tool()
|
|
1867
|
+
async def get_flow_trends(
|
|
1868
|
+
site_id: str,
|
|
1869
|
+
time_range: str = "7d",
|
|
1870
|
+
interval: str = "1h",
|
|
1871
|
+
) -> list[dict]:
|
|
1872
|
+
"""Get historical flow trends."""
|
|
1873
|
+
return await traffic_flows_tools.get_flow_trends(site_id, settings, time_range, interval)
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
@mcp.tool()
|
|
1877
|
+
async def filter_traffic_flows(
|
|
1878
|
+
site_id: str,
|
|
1879
|
+
filter_expression: str,
|
|
1880
|
+
time_range: str = "24h",
|
|
1881
|
+
limit: int | None = None,
|
|
1882
|
+
) -> list[dict]:
|
|
1883
|
+
"""Filter flows using a complex filter expression."""
|
|
1884
|
+
return await traffic_flows_tools.filter_traffic_flows(
|
|
1885
|
+
site_id, settings, filter_expression, time_range, limit
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
# Traffic Matching Lists Tools
|
|
1890
|
+
@mcp.tool()
|
|
1891
|
+
async def list_traffic_matching_lists(
|
|
1892
|
+
site_id: str,
|
|
1893
|
+
limit: int | None = None,
|
|
1894
|
+
offset: int | None = None,
|
|
1895
|
+
) -> list[dict]:
|
|
1896
|
+
"""List all traffic matching lists in a site (read-only)."""
|
|
1897
|
+
return await tml_tools.list_traffic_matching_lists(site_id, settings, limit, offset)
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
@mcp.tool()
|
|
1901
|
+
async def get_traffic_matching_list(site_id: str, list_id: str) -> dict:
|
|
1902
|
+
"""Get details for a specific traffic matching list."""
|
|
1903
|
+
return await tml_tools.get_traffic_matching_list(site_id, list_id, settings)
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
@mcp.tool()
|
|
1907
|
+
async def create_traffic_matching_list(
|
|
1908
|
+
site_id: str,
|
|
1909
|
+
list_type: str,
|
|
1910
|
+
name: str,
|
|
1911
|
+
items: list[str],
|
|
1912
|
+
confirm: bool = False,
|
|
1913
|
+
dry_run: bool = False,
|
|
1914
|
+
) -> dict:
|
|
1915
|
+
"""Create a new traffic matching list (requires confirm=True)."""
|
|
1916
|
+
return await tml_tools.create_traffic_matching_list(
|
|
1917
|
+
site_id, list_type, name, items, settings, confirm, dry_run
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
@mcp.tool()
|
|
1922
|
+
async def update_traffic_matching_list(
|
|
1923
|
+
site_id: str,
|
|
1924
|
+
list_id: str,
|
|
1925
|
+
list_type: str | None = None,
|
|
1926
|
+
name: str | None = None,
|
|
1927
|
+
items: list[str] | None = None,
|
|
1928
|
+
confirm: bool = False,
|
|
1929
|
+
dry_run: bool = False,
|
|
1930
|
+
) -> dict:
|
|
1931
|
+
"""Update an existing traffic matching list (requires confirm=True)."""
|
|
1932
|
+
return await tml_tools.update_traffic_matching_list(
|
|
1933
|
+
site_id, list_id, settings, list_type, name, items, confirm, dry_run
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
@mcp.tool()
|
|
1938
|
+
async def delete_traffic_matching_list(
|
|
1939
|
+
site_id: str,
|
|
1940
|
+
list_id: str,
|
|
1941
|
+
confirm: bool = False,
|
|
1942
|
+
dry_run: bool = False,
|
|
1943
|
+
) -> dict:
|
|
1944
|
+
"""Delete a traffic matching list (requires confirm=True)."""
|
|
1945
|
+
return await tml_tools.delete_traffic_matching_list(
|
|
1946
|
+
site_id, list_id, settings, confirm, dry_run
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
# Network Topology Tools
|
|
1951
|
+
@mcp.tool()
|
|
1952
|
+
async def get_network_topology(
|
|
1953
|
+
site_id: str,
|
|
1954
|
+
include_coordinates: bool = False,
|
|
1955
|
+
) -> dict:
|
|
1956
|
+
"""
|
|
1957
|
+
Retrieve complete network topology graph.
|
|
1958
|
+
|
|
1959
|
+
Fetches the network topology including all devices, clients, and their
|
|
1960
|
+
interconnections. Optionally includes position coordinates for visualization.
|
|
1961
|
+
|
|
1962
|
+
Args:
|
|
1963
|
+
site_id: Site identifier ("default" for default site)
|
|
1964
|
+
include_coordinates: Whether to calculate node position coordinates
|
|
1965
|
+
|
|
1966
|
+
Returns:
|
|
1967
|
+
Network diagram with nodes, connections, and statistics
|
|
1968
|
+
"""
|
|
1969
|
+
return await topology_tools.get_network_topology(site_id, settings, include_coordinates)
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
@mcp.tool()
|
|
1973
|
+
async def get_device_connections(
|
|
1974
|
+
site_id: str,
|
|
1975
|
+
device_id: str | None = None,
|
|
1976
|
+
) -> list[dict]:
|
|
1977
|
+
"""
|
|
1978
|
+
Get device interconnection details.
|
|
1979
|
+
|
|
1980
|
+
Retrieves detailed connection information for a specific device or all devices.
|
|
1981
|
+
|
|
1982
|
+
Args:
|
|
1983
|
+
site_id: Site identifier
|
|
1984
|
+
device_id: Specific device ID, or None for all devices
|
|
1985
|
+
|
|
1986
|
+
Returns:
|
|
1987
|
+
List of connection dictionaries
|
|
1988
|
+
"""
|
|
1989
|
+
return await topology_tools.get_device_connections(site_id, device_id, settings)
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
@mcp.tool()
|
|
1993
|
+
async def get_port_mappings(
|
|
1994
|
+
site_id: str,
|
|
1995
|
+
device_id: str,
|
|
1996
|
+
) -> dict:
|
|
1997
|
+
"""
|
|
1998
|
+
Get port-level connection mappings for a device.
|
|
1999
|
+
|
|
2000
|
+
Retrieves detailed information about which ports are connected to which devices/clients.
|
|
2001
|
+
|
|
2002
|
+
Args:
|
|
2003
|
+
site_id: Site identifier
|
|
2004
|
+
device_id: Device ID
|
|
2005
|
+
|
|
2006
|
+
Returns:
|
|
2007
|
+
Dictionary with device_id and port mapping information
|
|
2008
|
+
"""
|
|
2009
|
+
return await topology_tools.get_port_mappings(site_id, device_id, settings)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
@mcp.tool()
|
|
2013
|
+
async def export_topology(
|
|
2014
|
+
site_id: str,
|
|
2015
|
+
format: str,
|
|
2016
|
+
) -> str:
|
|
2017
|
+
"""
|
|
2018
|
+
Export network topology in various formats.
|
|
2019
|
+
|
|
2020
|
+
Exports the network topology as JSON, GraphML (XML), or DOT (Graphviz) format.
|
|
2021
|
+
|
|
2022
|
+
Args:
|
|
2023
|
+
site_id: Site identifier
|
|
2024
|
+
format: Export format ("json", "graphml", or "dot")
|
|
2025
|
+
|
|
2026
|
+
Returns:
|
|
2027
|
+
Topology data as a formatted string
|
|
2028
|
+
"""
|
|
2029
|
+
return await topology_tools.export_topology(site_id, format, settings) # type: ignore
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
@mcp.tool()
|
|
2033
|
+
async def get_topology_statistics(
|
|
2034
|
+
site_id: str,
|
|
2035
|
+
) -> dict:
|
|
2036
|
+
"""
|
|
2037
|
+
Get network topology statistics.
|
|
2038
|
+
|
|
2039
|
+
Retrieves statistical summary of the network topology including device counts,
|
|
2040
|
+
client counts, connection counts, and network depth.
|
|
2041
|
+
|
|
2042
|
+
Args:
|
|
2043
|
+
site_id: Site identifier
|
|
2044
|
+
|
|
2045
|
+
Returns:
|
|
2046
|
+
Dictionary with topology statistics
|
|
2047
|
+
"""
|
|
2048
|
+
return await topology_tools.get_topology_statistics(site_id, settings)
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
# VPN Management Tools
|
|
2052
|
+
@mcp.tool()
|
|
2053
|
+
async def list_vpn_tunnels(
|
|
2054
|
+
site_id: str,
|
|
2055
|
+
limit: int | None = None,
|
|
2056
|
+
offset: int | None = None,
|
|
2057
|
+
) -> list[dict]:
|
|
2058
|
+
"""List all site-to-site VPN tunnels (read-only)."""
|
|
2059
|
+
return await vpn_tools.list_vpn_tunnels(site_id, settings, limit, offset)
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
@mcp.tool()
|
|
2063
|
+
async def list_vpn_servers(
|
|
2064
|
+
site_id: str,
|
|
2065
|
+
limit: int | None = None,
|
|
2066
|
+
offset: int | None = None,
|
|
2067
|
+
) -> list[dict]:
|
|
2068
|
+
"""List all VPN servers (read-only)."""
|
|
2069
|
+
return await vpn_tools.list_vpn_servers(site_id, settings, limit, offset)
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
@mcp.tool()
|
|
2073
|
+
async def list_site_to_site_vpns(site_id: str) -> list[dict]:
|
|
2074
|
+
"""List all site-to-site IPsec VPN configurations."""
|
|
2075
|
+
return await site_vpn_tools.list_site_to_site_vpns(site_id, settings)
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
@mcp.tool()
|
|
2079
|
+
async def get_site_to_site_vpn(site_id: str, vpn_id: str) -> dict:
|
|
2080
|
+
"""Get details for a specific site-to-site VPN."""
|
|
2081
|
+
return await site_vpn_tools.get_site_to_site_vpn(site_id, vpn_id, settings)
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
@mcp.tool()
|
|
2085
|
+
async def update_site_to_site_vpn(
|
|
2086
|
+
site_id: str,
|
|
2087
|
+
vpn_id: str,
|
|
2088
|
+
name: str | None = None,
|
|
2089
|
+
enabled: bool | None = None,
|
|
2090
|
+
ipsec_peer_ip: str | None = None,
|
|
2091
|
+
remote_vpn_subnets: list[str] | None = None,
|
|
2092
|
+
x_ipsec_pre_shared_key: str | None = None,
|
|
2093
|
+
confirm: bool = False,
|
|
2094
|
+
dry_run: bool = False,
|
|
2095
|
+
) -> dict:
|
|
2096
|
+
"""Update a site-to-site VPN configuration (requires confirm=True)."""
|
|
2097
|
+
return await site_vpn_tools.update_site_to_site_vpn(
|
|
2098
|
+
site_id,
|
|
2099
|
+
vpn_id,
|
|
2100
|
+
settings,
|
|
2101
|
+
name=name,
|
|
2102
|
+
enabled=enabled,
|
|
2103
|
+
ipsec_peer_ip=ipsec_peer_ip,
|
|
2104
|
+
remote_vpn_subnets=remote_vpn_subnets,
|
|
2105
|
+
x_ipsec_pre_shared_key=x_ipsec_pre_shared_key,
|
|
2106
|
+
confirm=confirm,
|
|
2107
|
+
dry_run=dry_run,
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
# Reference Data Tools
|
|
2112
|
+
@mcp.tool()
|
|
2113
|
+
async def list_device_tags(
|
|
2114
|
+
site_id: str,
|
|
2115
|
+
limit: int | None = None,
|
|
2116
|
+
offset: int | None = None,
|
|
2117
|
+
) -> list[dict]:
|
|
2118
|
+
"""List all device tags in a site (read-only)."""
|
|
2119
|
+
return await ref_tools.list_device_tags(site_id, settings, limit, offset)
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
# Site Manager Tools
|
|
2123
|
+
@mcp.tool()
|
|
2124
|
+
async def list_all_sites_aggregated() -> list[dict]:
|
|
2125
|
+
"""List all sites with aggregated stats from Site Manager API."""
|
|
2126
|
+
return await site_manager_tools.list_all_sites_aggregated(settings)
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
@mcp.tool()
|
|
2130
|
+
async def get_internet_health(site_id: str | None = None) -> dict:
|
|
2131
|
+
"""Get internet health metrics across sites."""
|
|
2132
|
+
return await site_manager_tools.get_internet_health(settings, site_id)
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
@mcp.tool()
|
|
2136
|
+
async def get_site_health_summary(site_id: str | None = None) -> dict:
|
|
2137
|
+
"""Get health summary for all sites or a specific site."""
|
|
2138
|
+
return await site_manager_tools.get_site_health_summary(settings, site_id) # type: ignore[return-value]
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
@mcp.tool()
|
|
2142
|
+
async def get_cross_site_statistics() -> dict:
|
|
2143
|
+
"""Get aggregate statistics across multiple sites."""
|
|
2144
|
+
return await site_manager_tools.get_cross_site_statistics(settings)
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
@mcp.tool()
|
|
2148
|
+
async def list_vantage_points() -> list[dict]:
|
|
2149
|
+
"""List all Vantage Points."""
|
|
2150
|
+
return await site_manager_tools.list_vantage_points(settings)
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
@mcp.tool()
|
|
2154
|
+
async def get_site_inventory(site_id: str | None = None) -> dict:
|
|
2155
|
+
"""Get comprehensive inventory for a site or all sites."""
|
|
2156
|
+
return await site_manager_tools.get_site_inventory(settings, site_id) # type: ignore[return-value]
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
@mcp.tool()
|
|
2160
|
+
async def compare_site_performance() -> dict:
|
|
2161
|
+
"""Compare performance metrics across all sites."""
|
|
2162
|
+
return await site_manager_tools.compare_site_performance(settings)
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
@mcp.tool()
|
|
2166
|
+
async def search_across_sites(query: str, search_type: str = "all") -> dict:
|
|
2167
|
+
"""Search for resources across all sites (device/client/network)."""
|
|
2168
|
+
return await site_manager_tools.search_across_sites(settings, query, search_type)
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
# Additional MCP Resources
|
|
2172
|
+
# ⚠️ REMOVED: sites://{site_id}/firewall/matrix resource
|
|
2173
|
+
# ZBF matrix endpoint does not exist in UniFi API v10.0.156
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
@mcp.resource("sites://{site_id}/traffic/flows")
|
|
2177
|
+
async def get_traffic_flows_resource(site_id: str) -> str:
|
|
2178
|
+
"""Get traffic flows for a site.
|
|
2179
|
+
|
|
2180
|
+
Args:
|
|
2181
|
+
site_id: Site identifier
|
|
2182
|
+
|
|
2183
|
+
Returns:
|
|
2184
|
+
JSON string of traffic flows
|
|
2185
|
+
"""
|
|
2186
|
+
flows = await traffic_flows_tools.get_traffic_flows(site_id, settings)
|
|
2187
|
+
import json
|
|
2188
|
+
|
|
2189
|
+
return json.dumps(flows, indent=2)
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
@mcp.resource("site-manager://sites")
|
|
2193
|
+
async def get_site_manager_sites_resource() -> str:
|
|
2194
|
+
"""Get all sites from Site Manager API.
|
|
2195
|
+
|
|
2196
|
+
Returns:
|
|
2197
|
+
JSON string of sites list
|
|
2198
|
+
"""
|
|
2199
|
+
return await site_manager_res.get_all_sites()
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
@mcp.resource("site-manager://health")
|
|
2203
|
+
async def get_site_manager_health_resource() -> str:
|
|
2204
|
+
"""Get cross-site health metrics.
|
|
2205
|
+
|
|
2206
|
+
Returns:
|
|
2207
|
+
JSON string of health metrics
|
|
2208
|
+
"""
|
|
2209
|
+
return await site_manager_res.get_health_metrics()
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
@mcp.resource("site-manager://internet-health")
|
|
2213
|
+
async def get_site_manager_internet_health_resource() -> str:
|
|
2214
|
+
"""Get internet connectivity status.
|
|
2215
|
+
|
|
2216
|
+
Returns:
|
|
2217
|
+
JSON string of internet health
|
|
2218
|
+
"""
|
|
2219
|
+
return await site_manager_res.get_internet_health_status()
|
|
2220
|
+
|
|
2221
|
+
|
|
2222
|
+
def main() -> None:
|
|
2223
|
+
"""Main entry point for the MCP server."""
|
|
2224
|
+
logger.info("Starting UniFi MCP Server...")
|
|
2225
|
+
logger.info(f"API Type: {settings.api_type.value}")
|
|
2226
|
+
logger.info(f"Base URL: {settings.base_url}")
|
|
2227
|
+
logger.info("Server ready to handle requests")
|
|
2228
|
+
|
|
2229
|
+
# Start the FastMCP server
|
|
2230
|
+
mcp.run()
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
if __name__ == "__main__":
|
|
2234
|
+
main()
|