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/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]
|