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.
Files changed (81) hide show
  1. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
  2. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
  3. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
  4. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
  6. src/__init__.py +3 -0
  7. src/__main__.py +6 -0
  8. src/api/__init__.py +5 -0
  9. src/api/client.py +727 -0
  10. src/api/site_manager_client.py +176 -0
  11. src/cache.py +483 -0
  12. src/config/__init__.py +5 -0
  13. src/config/config.py +321 -0
  14. src/main.py +2234 -0
  15. src/models/__init__.py +126 -0
  16. src/models/acl.py +41 -0
  17. src/models/backup.py +272 -0
  18. src/models/client.py +74 -0
  19. src/models/device.py +53 -0
  20. src/models/dpi.py +50 -0
  21. src/models/firewall_policy.py +123 -0
  22. src/models/firewall_zone.py +28 -0
  23. src/models/network.py +62 -0
  24. src/models/qos_profile.py +458 -0
  25. src/models/radius.py +141 -0
  26. src/models/reference_data.py +34 -0
  27. src/models/site.py +59 -0
  28. src/models/site_manager.py +120 -0
  29. src/models/topology.py +138 -0
  30. src/models/traffic_flow.py +137 -0
  31. src/models/traffic_matching_list.py +56 -0
  32. src/models/voucher.py +42 -0
  33. src/models/vpn.py +73 -0
  34. src/models/wan.py +48 -0
  35. src/models/zbf_matrix.py +49 -0
  36. src/resources/__init__.py +8 -0
  37. src/resources/clients.py +111 -0
  38. src/resources/devices.py +102 -0
  39. src/resources/networks.py +93 -0
  40. src/resources/site_manager.py +64 -0
  41. src/resources/sites.py +86 -0
  42. src/tools/__init__.py +25 -0
  43. src/tools/acls.py +328 -0
  44. src/tools/application.py +42 -0
  45. src/tools/backups.py +1173 -0
  46. src/tools/client_management.py +505 -0
  47. src/tools/clients.py +203 -0
  48. src/tools/device_control.py +325 -0
  49. src/tools/devices.py +354 -0
  50. src/tools/dpi.py +241 -0
  51. src/tools/dpi_tools.py +89 -0
  52. src/tools/firewall.py +417 -0
  53. src/tools/firewall_policies.py +430 -0
  54. src/tools/firewall_zones.py +515 -0
  55. src/tools/network_config.py +388 -0
  56. src/tools/networks.py +190 -0
  57. src/tools/port_forwarding.py +263 -0
  58. src/tools/qos.py +1070 -0
  59. src/tools/radius.py +763 -0
  60. src/tools/reference_data.py +107 -0
  61. src/tools/site_manager.py +466 -0
  62. src/tools/site_vpn.py +95 -0
  63. src/tools/sites.py +187 -0
  64. src/tools/topology.py +406 -0
  65. src/tools/traffic_flows.py +1062 -0
  66. src/tools/traffic_matching_lists.py +371 -0
  67. src/tools/vouchers.py +249 -0
  68. src/tools/vpn.py +76 -0
  69. src/tools/wans.py +30 -0
  70. src/tools/wifi.py +498 -0
  71. src/tools/zbf_matrix.py +326 -0
  72. src/utils/__init__.py +88 -0
  73. src/utils/audit.py +213 -0
  74. src/utils/exceptions.py +114 -0
  75. src/utils/helpers.py +159 -0
  76. src/utils/logger.py +105 -0
  77. src/utils/sanitize.py +244 -0
  78. src/utils/validators.py +160 -0
  79. src/webhooks/__init__.py +6 -0
  80. src/webhooks/handlers.py +196 -0
  81. 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()