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/tools/dpi.py ADDED
@@ -0,0 +1,241 @@
1
+ """Deep Packet Inspection (DPI) statistics MCP tools."""
2
+
3
+ from typing import Any
4
+
5
+ from ..api import UniFiClient
6
+ from ..config import Settings
7
+ from ..utils import (
8
+ get_logger,
9
+ sanitize_log_message,
10
+ validate_limit_offset,
11
+ validate_mac_address,
12
+ validate_site_id,
13
+ )
14
+
15
+
16
+ async def get_dpi_statistics(
17
+ site_id: str,
18
+ settings: Settings,
19
+ time_range: str = "24h",
20
+ ) -> dict[str, Any]:
21
+ """Get Deep Packet Inspection statistics.
22
+
23
+ Args:
24
+ site_id: Site identifier
25
+ settings: Application settings
26
+ time_range: Time range for statistics (1h, 6h, 12h, 24h, 7d, 30d)
27
+
28
+ Returns:
29
+ DPI statistics dictionary
30
+ """
31
+ site_id = validate_site_id(site_id)
32
+ logger = get_logger(__name__, settings.log_level)
33
+
34
+ # Validate time range
35
+ valid_ranges = ["1h", "6h", "12h", "24h", "7d", "30d"]
36
+ if time_range not in valid_ranges:
37
+ raise ValueError(f"Invalid time range '{time_range}'. Must be one of: {valid_ranges}")
38
+
39
+ async with UniFiClient(settings) as client:
40
+ await client.authenticate()
41
+
42
+ # Get DPI statistics
43
+ response = await client.get(f"/ea/sites/{site_id}/stat/dpi")
44
+ # Handle both list and dict responses
45
+ dpi_data = response if isinstance(response, list) else response.get("data", [])
46
+
47
+ # Aggregate by application/category
48
+ app_stats = {}
49
+ category_stats = {}
50
+
51
+ for entry in dpi_data:
52
+ app = entry.get("app")
53
+ cat = entry.get("cat")
54
+ tx_bytes = entry.get("tx_bytes", 0)
55
+ rx_bytes = entry.get("rx_bytes", 0)
56
+ total_bytes = tx_bytes + rx_bytes
57
+
58
+ # Aggregate by application
59
+ if app:
60
+ if app not in app_stats:
61
+ app_stats[app] = {
62
+ "application": app,
63
+ "category": cat,
64
+ "tx_bytes": 0,
65
+ "rx_bytes": 0,
66
+ "total_bytes": 0,
67
+ }
68
+ app_stats[app]["tx_bytes"] += tx_bytes
69
+ app_stats[app]["rx_bytes"] += rx_bytes
70
+ app_stats[app]["total_bytes"] += total_bytes
71
+
72
+ # Aggregate by category
73
+ if cat:
74
+ if cat not in category_stats:
75
+ category_stats[cat] = {
76
+ "category": cat,
77
+ "tx_bytes": 0,
78
+ "rx_bytes": 0,
79
+ "total_bytes": 0,
80
+ "application_count": 0,
81
+ }
82
+ category_stats[cat]["tx_bytes"] += tx_bytes
83
+ category_stats[cat]["rx_bytes"] += rx_bytes
84
+ category_stats[cat]["total_bytes"] += total_bytes
85
+ if app:
86
+ category_stats[cat]["application_count"] += 1
87
+
88
+ # Convert to lists and sort by total bytes
89
+ applications = sorted(app_stats.values(), key=lambda x: x["total_bytes"], reverse=True)
90
+ categories = sorted(category_stats.values(), key=lambda x: x["total_bytes"], reverse=True)
91
+
92
+ logger.info(
93
+ sanitize_log_message(
94
+ f"Retrieved DPI statistics for site '{site_id}' " f"(time range: {time_range})"
95
+ )
96
+ )
97
+
98
+ return {
99
+ "site_id": site_id,
100
+ "time_range": time_range,
101
+ "applications": applications,
102
+ "categories": categories,
103
+ "total_applications": len(applications),
104
+ "total_categories": len(categories),
105
+ }
106
+
107
+
108
+ async def list_top_applications(
109
+ site_id: str,
110
+ settings: Settings,
111
+ limit: int = 10,
112
+ time_range: str = "24h",
113
+ ) -> list[dict[str, Any]]:
114
+ """List top applications by bandwidth usage.
115
+
116
+ Args:
117
+ site_id: Site identifier
118
+ settings: Application settings
119
+ limit: Number of top applications to return
120
+ time_range: Time range for statistics (1h, 6h, 12h, 24h, 7d, 30d)
121
+
122
+ Returns:
123
+ List of top application dictionaries sorted by bandwidth
124
+ """
125
+ site_id = validate_site_id(site_id)
126
+ logger = get_logger(__name__, settings.log_level)
127
+
128
+ # Get full DPI statistics
129
+ dpi_stats = await get_dpi_statistics(site_id, settings, time_range)
130
+
131
+ # Get top N applications
132
+ top_apps: list[dict[str, Any]] = dpi_stats["applications"][:limit]
133
+
134
+ logger.info(
135
+ sanitize_log_message(
136
+ f"Retrieved top {len(top_apps)} applications for site '{site_id}' "
137
+ f"(time range: {time_range}"
138
+ )
139
+ )
140
+
141
+ return top_apps
142
+
143
+
144
+ async def get_client_dpi(
145
+ site_id: str,
146
+ client_mac: str,
147
+ settings: Settings,
148
+ time_range: str = "24h",
149
+ limit: int | None = None,
150
+ offset: int | None = None,
151
+ ) -> dict[str, Any]:
152
+ """Get DPI statistics for a specific client.
153
+
154
+ Args:
155
+ site_id: Site identifier
156
+ client_mac: Client MAC address
157
+ settings: Application settings
158
+ time_range: Time range for statistics (1h, 6h, 12h, 24h, 7d, 30d)
159
+ limit: Maximum number of applications to return
160
+ offset: Number of applications to skip
161
+
162
+ Returns:
163
+ Client DPI statistics dictionary
164
+ """
165
+ site_id = validate_site_id(site_id)
166
+ client_mac = validate_mac_address(client_mac)
167
+ limit, offset = validate_limit_offset(limit, offset)
168
+ logger = get_logger(__name__, settings.log_level)
169
+
170
+ # Validate time range
171
+ valid_ranges = ["1h", "6h", "12h", "24h", "7d", "30d"]
172
+ if time_range not in valid_ranges:
173
+ raise ValueError(f"Invalid time range '{time_range}'. Must be one of: {valid_ranges}")
174
+
175
+ async with UniFiClient(settings) as client:
176
+ await client.authenticate()
177
+
178
+ # Get client-specific DPI data
179
+ response = await client.get(f"/ea/sites/{site_id}/stat/stadpi/{client_mac}")
180
+ # Handle both list and dict responses
181
+ dpi_data = response if isinstance(response, list) else response.get("data", [])
182
+
183
+ # Aggregate by application
184
+ app_stats = {}
185
+ total_tx = 0
186
+ total_rx = 0
187
+
188
+ for entry in dpi_data:
189
+ app = entry.get("app")
190
+ cat = entry.get("cat")
191
+ tx_bytes = entry.get("tx_bytes", 0)
192
+ rx_bytes = entry.get("rx_bytes", 0)
193
+ total_bytes = tx_bytes + rx_bytes
194
+
195
+ total_tx += tx_bytes
196
+ total_rx += rx_bytes
197
+
198
+ if app:
199
+ if app not in app_stats:
200
+ app_stats[app] = {
201
+ "application": app,
202
+ "category": cat,
203
+ "tx_bytes": 0,
204
+ "rx_bytes": 0,
205
+ "total_bytes": 0,
206
+ }
207
+ app_stats[app]["tx_bytes"] += tx_bytes
208
+ app_stats[app]["rx_bytes"] += rx_bytes
209
+ app_stats[app]["total_bytes"] += total_bytes
210
+
211
+ # Convert to list and sort by total bytes
212
+ applications = sorted(app_stats.values(), key=lambda x: x["total_bytes"], reverse=True)
213
+
214
+ # Apply pagination
215
+ paginated_apps = applications[offset : offset + limit]
216
+
217
+ # Calculate percentages
218
+ total_bytes = total_tx + total_rx
219
+ for app in paginated_apps:
220
+ if total_bytes > 0:
221
+ app["percentage"] = (app["total_bytes"] / total_bytes) * 100
222
+ else:
223
+ app["percentage"] = 0
224
+
225
+ logger.info(
226
+ sanitize_log_message(
227
+ f"Retrieved DPI statistics for client '{client_mac}' in site '{site_id}' "
228
+ f"(time range: {time_range})"
229
+ )
230
+ )
231
+
232
+ return {
233
+ "site_id": site_id,
234
+ "client_mac": client_mac,
235
+ "time_range": time_range,
236
+ "total_tx_bytes": total_tx,
237
+ "total_rx_bytes": total_rx,
238
+ "total_bytes": total_bytes,
239
+ "applications": paginated_apps,
240
+ "total_applications": len(applications),
241
+ }
src/tools/dpi_tools.py ADDED
@@ -0,0 +1,89 @@
1
+ """DPI (Deep Packet Inspection) and country information tools."""
2
+
3
+ from typing import Any
4
+
5
+ from ..api.client import UniFiClient
6
+ from ..config import Settings
7
+ from ..models import Country, DPIApplication, DPICategory
8
+ from ..utils import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ async def list_dpi_categories(settings: Settings) -> list[dict]:
14
+ """List all DPI categories.
15
+
16
+ Args:
17
+ settings: Application settings
18
+
19
+ Returns:
20
+ List of DPI categories
21
+ """
22
+ async with UniFiClient(settings) as client:
23
+ logger.info("Listing DPI categories")
24
+
25
+ if not client.is_authenticated:
26
+ await client.authenticate()
27
+
28
+ response = await client.get("/integration/v1/dpi/categories")
29
+ data = response.get("data", [])
30
+
31
+ return [DPICategory(**category).model_dump() for category in data]
32
+
33
+
34
+ async def list_dpi_applications(
35
+ settings: Settings,
36
+ limit: int | None = None,
37
+ offset: int | None = None,
38
+ filter_expr: str | None = None,
39
+ ) -> list[dict]:
40
+ """List all DPI applications.
41
+
42
+ Args:
43
+ settings: Application settings
44
+ limit: Maximum number of results
45
+ offset: Starting position
46
+ filter_expr: Filter expression
47
+
48
+ Returns:
49
+ List of DPI applications
50
+ """
51
+ async with UniFiClient(settings) as client:
52
+ logger.info("Listing DPI applications")
53
+
54
+ if not client.is_authenticated:
55
+ await client.authenticate()
56
+
57
+ params: dict[str, Any] = {}
58
+ if limit is not None:
59
+ params["limit"] = limit
60
+ if offset is not None:
61
+ params["offset"] = offset
62
+ if filter_expr:
63
+ params["filter"] = filter_expr
64
+
65
+ response = await client.get("/integration/v1/dpi/applications", params=params)
66
+ data = response.get("data", [])
67
+
68
+ return [DPIApplication(**app).model_dump() for app in data]
69
+
70
+
71
+ async def list_countries(settings: Settings) -> list[dict]:
72
+ """List all countries for configuration and localization.
73
+
74
+ Args:
75
+ settings: Application settings
76
+
77
+ Returns:
78
+ List of countries
79
+ """
80
+ async with UniFiClient(settings) as client:
81
+ logger.info("Listing countries")
82
+
83
+ if not client.is_authenticated:
84
+ await client.authenticate()
85
+
86
+ response = await client.get("/integration/v1/countries")
87
+ data = response.get("data", [])
88
+
89
+ return [Country(**country).model_dump() for country in data]