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/config/config.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Configuration management for UniFi MCP Server using Pydantic Settings."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, field_validator, model_validator
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIType(str, Enum):
|
|
11
|
+
"""API connection type enumeration."""
|
|
12
|
+
|
|
13
|
+
CLOUD_V1 = "cloud-v1" # Official stable v1 API
|
|
14
|
+
CLOUD_EA = "cloud-ea" # Early Access API
|
|
15
|
+
LOCAL = "local" # Direct gateway access
|
|
16
|
+
|
|
17
|
+
# Legacy alias for backward compatibility (defaults to EA)
|
|
18
|
+
CLOUD = "cloud-ea"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Settings(BaseSettings):
|
|
22
|
+
"""Application settings loaded from environment variables and .env file."""
|
|
23
|
+
|
|
24
|
+
model_config = SettingsConfigDict(
|
|
25
|
+
env_file=".env",
|
|
26
|
+
env_file_encoding="utf-8",
|
|
27
|
+
case_sensitive=False,
|
|
28
|
+
extra="ignore",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# API Configuration
|
|
32
|
+
api_key: str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description="UniFi API key (X-API-Key header)",
|
|
35
|
+
validation_alias="UNIFI_API_KEY",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
api_type: APIType = Field(
|
|
39
|
+
default=APIType.CLOUD_EA,
|
|
40
|
+
description="API connection type: 'cloud-v1' (stable), 'cloud-ea' (early access), or 'local' (gateway)",
|
|
41
|
+
validation_alias="UNIFI_API_TYPE",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Cloud API Configuration
|
|
45
|
+
cloud_api_url: str = Field(
|
|
46
|
+
default="https://api.ui.com",
|
|
47
|
+
description="UniFi Cloud API base URL",
|
|
48
|
+
validation_alias="UNIFI_CLOUD_API_URL",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Local API Configuration
|
|
52
|
+
local_host: str | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="Local UniFi controller hostname/IP",
|
|
55
|
+
validation_alias="UNIFI_LOCAL_HOST",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
local_port: int = Field(
|
|
59
|
+
default=443,
|
|
60
|
+
description="Local UniFi controller port",
|
|
61
|
+
validation_alias="UNIFI_LOCAL_PORT",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
local_verify_ssl: bool = Field(
|
|
65
|
+
default=True,
|
|
66
|
+
description="Verify SSL certificates for local controller",
|
|
67
|
+
validation_alias="UNIFI_LOCAL_VERIFY_SSL",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Site Configuration
|
|
71
|
+
default_site: str = Field(
|
|
72
|
+
default="default",
|
|
73
|
+
description="Default site ID to use",
|
|
74
|
+
validation_alias="UNIFI_DEFAULT_SITE",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Site Manager API Configuration
|
|
78
|
+
site_manager_enabled: bool = Field(
|
|
79
|
+
default=False,
|
|
80
|
+
description="Enable Site Manager API (multi-site management)",
|
|
81
|
+
validation_alias="UNIFI_SITE_MANAGER_ENABLED",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Rate Limiting Configuration
|
|
85
|
+
rate_limit_requests: int = Field(
|
|
86
|
+
default=100,
|
|
87
|
+
description="Maximum requests per minute (EA tier: 100, v1 tier: 10000)",
|
|
88
|
+
validation_alias="UNIFI_RATE_LIMIT_REQUESTS",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
rate_limit_period: int = Field(
|
|
92
|
+
default=60,
|
|
93
|
+
description="Rate limit period in seconds",
|
|
94
|
+
validation_alias="UNIFI_RATE_LIMIT_PERIOD",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Retry Configuration
|
|
98
|
+
max_retries: int = Field(
|
|
99
|
+
default=3,
|
|
100
|
+
description="Maximum number of retry attempts for failed requests",
|
|
101
|
+
validation_alias="UNIFI_MAX_RETRIES",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
retry_backoff_factor: float = Field(
|
|
105
|
+
default=2.0,
|
|
106
|
+
description="Exponential backoff factor for retries",
|
|
107
|
+
validation_alias="UNIFI_RETRY_BACKOFF_FACTOR",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Timeout Configuration
|
|
111
|
+
request_timeout: int = Field(
|
|
112
|
+
default=30,
|
|
113
|
+
description="Request timeout in seconds",
|
|
114
|
+
validation_alias="UNIFI_REQUEST_TIMEOUT",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Caching Configuration
|
|
118
|
+
cache_enabled: bool = Field(
|
|
119
|
+
default=True,
|
|
120
|
+
description="Enable response caching",
|
|
121
|
+
validation_alias="UNIFI_CACHE_ENABLED",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
cache_ttl: int = Field(
|
|
125
|
+
default=300,
|
|
126
|
+
description="Cache TTL in seconds (default: 5 minutes)",
|
|
127
|
+
validation_alias="UNIFI_CACHE_TTL",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Logging Configuration
|
|
131
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
|
|
132
|
+
default="INFO",
|
|
133
|
+
description="Logging level",
|
|
134
|
+
validation_alias="LOG_LEVEL",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
log_api_requests: bool = Field(
|
|
138
|
+
default=True,
|
|
139
|
+
description="Log all API requests",
|
|
140
|
+
validation_alias="LOG_API_REQUESTS",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Audit Logging
|
|
144
|
+
audit_log_enabled: bool = Field(
|
|
145
|
+
default=True,
|
|
146
|
+
description="Enable audit logging for mutating operations",
|
|
147
|
+
validation_alias="UNIFI_AUDIT_LOG_ENABLED",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@field_validator("api_type", mode="before")
|
|
151
|
+
@classmethod
|
|
152
|
+
def validate_api_type(cls, v: str) -> APIType:
|
|
153
|
+
"""Validate and convert API type to enum.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
v: API type string
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
APIType enum value
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(v, APIType):
|
|
162
|
+
return v
|
|
163
|
+
return APIType(v.lower())
|
|
164
|
+
|
|
165
|
+
@field_validator("local_port")
|
|
166
|
+
@classmethod
|
|
167
|
+
def validate_port(cls, v: int) -> int:
|
|
168
|
+
"""Validate port number is in valid range.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
v: Port number
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Validated port number
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValueError: If port is invalid
|
|
178
|
+
"""
|
|
179
|
+
if not 1 <= v <= 65535:
|
|
180
|
+
raise ValueError(f"Port must be between 1 and 65535, got {v}")
|
|
181
|
+
return v
|
|
182
|
+
|
|
183
|
+
@model_validator(mode="after")
|
|
184
|
+
def validate_local_configuration(self) -> "Settings":
|
|
185
|
+
"""Validate that local API has required configuration.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Validated settings instance
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If local API is selected but host is not provided
|
|
192
|
+
"""
|
|
193
|
+
if self.api_type == APIType.LOCAL and not self.local_host:
|
|
194
|
+
raise ValueError("local_host is required when api_type is 'local'")
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def base_url(self) -> str:
|
|
199
|
+
"""Get the appropriate base URL based on API type.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Base URL for API requests
|
|
203
|
+
"""
|
|
204
|
+
if self.api_type in (APIType.CLOUD_V1, APIType.CLOUD_EA):
|
|
205
|
+
return self.cloud_api_url
|
|
206
|
+
else:
|
|
207
|
+
# Always use HTTPS for local gateways (port 443)
|
|
208
|
+
# SSL verification is controlled separately via verify_ssl property
|
|
209
|
+
return f"https://{self.local_host}:{self.local_port}"
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def verify_ssl(self) -> bool:
|
|
213
|
+
"""Get SSL verification setting based on API type.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Whether to verify SSL certificates
|
|
217
|
+
"""
|
|
218
|
+
if self.api_type in (APIType.CLOUD_V1, APIType.CLOUD_EA):
|
|
219
|
+
return True
|
|
220
|
+
return self.local_verify_ssl
|
|
221
|
+
|
|
222
|
+
def get_integration_path(self, endpoint: str) -> str:
|
|
223
|
+
"""Get the correct integration API endpoint path based on API type.
|
|
224
|
+
|
|
225
|
+
For Cloud V1 API: Returns /v1/{endpoint}
|
|
226
|
+
For Cloud EA API: Returns /integration/v1/{endpoint} (ZBF not supported on Cloud)
|
|
227
|
+
For Local API: Returns /proxy/network/integration/v1/{endpoint}
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
endpoint: The endpoint path starting with /sites/... (e.g., "/sites/default/firewall/zones")
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Complete endpoint path with correct prefix
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> settings.get_integration_path("/sites/abc/firewall/zones")
|
|
237
|
+
# Cloud V1: "/v1/sites/abc/firewall/zones"
|
|
238
|
+
# Cloud EA: "/integration/v1/sites/abc/firewall/zones"
|
|
239
|
+
# Local: "/proxy/network/integration/v1/sites/abc/firewall/zones"
|
|
240
|
+
"""
|
|
241
|
+
# Remove leading slash if present for consistency
|
|
242
|
+
endpoint = endpoint.lstrip("/")
|
|
243
|
+
|
|
244
|
+
if self.api_type == APIType.CLOUD_V1:
|
|
245
|
+
return f"/v1/{endpoint}"
|
|
246
|
+
elif self.api_type == APIType.CLOUD_EA:
|
|
247
|
+
return f"/integration/v1/{endpoint}"
|
|
248
|
+
else:
|
|
249
|
+
# Local gateways require /proxy/network/ prefix
|
|
250
|
+
return f"/proxy/network/integration/v1/{endpoint}"
|
|
251
|
+
|
|
252
|
+
def get_site_api_path(self, site_id: str, endpoint: str) -> str:
|
|
253
|
+
"""Get the correct standard UniFi API endpoint path based on API type.
|
|
254
|
+
|
|
255
|
+
For Cloud V1 API: Returns /v1/{endpoint} (site-less endpoints like /hosts)
|
|
256
|
+
For Cloud EA API: Returns /ea/sites/{site_id}/{endpoint}
|
|
257
|
+
For Local API: Returns /proxy/network/api/s/{site_id}/{endpoint}
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
site_id: The site ID (may be unused for Cloud V1 top-level endpoints)
|
|
261
|
+
endpoint: The endpoint path (e.g., "devices", "sta", "rest/networkconf")
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Complete endpoint path with correct prefix
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> settings.get_site_api_path("default", "devices")
|
|
268
|
+
# Cloud V1: "/v1/hosts" (devices are under hosts endpoint)
|
|
269
|
+
# Cloud EA: "/ea/sites/default/devices"
|
|
270
|
+
# Local: "/proxy/network/api/s/default/devices"
|
|
271
|
+
"""
|
|
272
|
+
# Remove leading slash if present for consistency
|
|
273
|
+
endpoint = endpoint.lstrip("/")
|
|
274
|
+
|
|
275
|
+
if self.api_type == APIType.CLOUD_V1:
|
|
276
|
+
# V1 API uses top-level endpoints without site_id in path
|
|
277
|
+
# Note: For v1, endpoints like "devices" are accessed via /v1/hosts
|
|
278
|
+
return f"/v1/{endpoint}"
|
|
279
|
+
elif self.api_type == APIType.CLOUD_EA:
|
|
280
|
+
return f"/ea/sites/{site_id}/{endpoint}"
|
|
281
|
+
else:
|
|
282
|
+
# Local gateways use /proxy/network/api/s/ prefix
|
|
283
|
+
return f"/proxy/network/api/s/{site_id}/{endpoint}"
|
|
284
|
+
|
|
285
|
+
def get_v2_api_path(self, site_id: str) -> str:
|
|
286
|
+
"""Get the v2 API endpoint path for local gateway access.
|
|
287
|
+
|
|
288
|
+
The v2 API is only available on local gateways and provides access to
|
|
289
|
+
features like firewall policies that are not available via the cloud API.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
site_id: The site identifier
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Complete endpoint path: /proxy/network/v2/api/site/{site_id}
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
NotImplementedError: If api_type is not LOCAL (v2 API only works locally)
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> settings.get_v2_api_path("default")
|
|
302
|
+
# Local: "/proxy/network/v2/api/site/default"
|
|
303
|
+
"""
|
|
304
|
+
if self.api_type != APIType.LOCAL:
|
|
305
|
+
raise NotImplementedError(
|
|
306
|
+
"v2 API is only available with local gateway access. "
|
|
307
|
+
"Set UNIFI_API_TYPE=local and configure UNIFI_LOCAL_HOST."
|
|
308
|
+
)
|
|
309
|
+
return f"/proxy/network/v2/api/site/{site_id}"
|
|
310
|
+
|
|
311
|
+
def get_headers(self) -> dict[str, str]:
|
|
312
|
+
"""Get HTTP headers for API requests.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dictionary of HTTP headers
|
|
316
|
+
"""
|
|
317
|
+
return {
|
|
318
|
+
"X-API-KEY": self.api_key, # UniFi API expects all caps
|
|
319
|
+
"Content-Type": "application/json",
|
|
320
|
+
"Accept": "application/json",
|
|
321
|
+
}
|