unifi-network-mcp 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/PKG-INFO +1 -1
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/pyproject.toml +1 -1
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/connection_manager.py +116 -39
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/lazy_tool_loader.py +86 -2
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/.gitignore +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/.well-known/mcp-server.json +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/LICENSE +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/README.md +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/bootstrap.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/config/config.yaml +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/jobs.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/main.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/client_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/device_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/event_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/firewall_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/hotspot_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/network_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/qos_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/routing_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/stats_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/system_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/traffic_route_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/usergroup_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/managers/vpn_manager.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/runtime.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/schemas.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tool_index.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/clients.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/config.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/devices.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/events.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/firewall.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/hotspot.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/network.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/port_forwards.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/qos.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/routing.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/stats.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/system.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/traffic_routes.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/usergroups.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools/vpn.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/tools_manifest.json +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/confirmation.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/diagnostics.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/meta_tools.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/permissions.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/utils/tool_loader.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/validator_registry.py +0 -0
- {unifi_network_mcp-0.3.1 → unifi_network_mcp-0.3.2}/src/validators.py +0 -0
|
@@ -13,11 +13,68 @@ from aiounifi.models.configuration import Configuration
|
|
|
13
13
|
logger = logging.getLogger("unifi-network-mcp")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
async def detect_unifi_os_pre_login(
|
|
17
|
+
session: aiohttp.ClientSession,
|
|
18
|
+
base_url: str,
|
|
19
|
+
timeout: int = 5,
|
|
20
|
+
) -> Optional[bool]:
|
|
21
|
+
"""
|
|
22
|
+
Detect UniFi OS BEFORE authentication using unauthenticated probes.
|
|
23
|
+
|
|
24
|
+
This detection determines which auth endpoint to use:
|
|
25
|
+
- UniFi OS: /api/auth/login
|
|
26
|
+
- Standalone: /api/login
|
|
27
|
+
|
|
28
|
+
Strategy:
|
|
29
|
+
1. GET base URL - UniFi OS returns 200 with HTML, standalone redirects or errors
|
|
30
|
+
2. Check for UniFi OS specific headers/behavior
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
session: Active aiohttp.ClientSession
|
|
34
|
+
base_url: Base URL of controller (e.g., 'https://192.168.1.1:443')
|
|
35
|
+
timeout: Detection timeout in seconds (default: 5)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True: UniFi OS detected (use /api/auth/login)
|
|
39
|
+
False: Standalone controller (use /api/login)
|
|
40
|
+
None: Detection inconclusive
|
|
41
|
+
"""
|
|
42
|
+
client_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Probe 1: GET base URL without following redirects
|
|
46
|
+
# UniFi OS typically returns 200 OK with the web UI
|
|
47
|
+
# Standalone controllers often redirect to /manage or return different status
|
|
48
|
+
async with session.get(base_url, timeout=client_timeout, ssl=False, allow_redirects=False) as response:
|
|
49
|
+
logger.debug(f"Pre-login probe {base_url}: status={response.status}")
|
|
50
|
+
|
|
51
|
+
if response.status == 200:
|
|
52
|
+
# UniFi OS returns 200 at base URL
|
|
53
|
+
logger.debug("Pre-login detection: UniFi OS (200 at base URL)")
|
|
54
|
+
return True
|
|
55
|
+
elif response.status in (301, 302, 303, 307, 308):
|
|
56
|
+
# Redirect typically indicates standalone controller
|
|
57
|
+
location = response.headers.get("Location", "")
|
|
58
|
+
logger.debug(f"Pre-login detection: redirect to {location}")
|
|
59
|
+
# Could be standalone redirecting to /manage
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
except asyncio.TimeoutError:
|
|
63
|
+
logger.debug("Pre-login detection: timeout")
|
|
64
|
+
except aiohttp.ClientError as e:
|
|
65
|
+
logger.debug(f"Pre-login detection failed: {e}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.debug(f"Pre-login detection unexpected error: {e}")
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
16
72
|
async def detect_with_retry(
|
|
17
73
|
session: aiohttp.ClientSession,
|
|
18
74
|
base_url: str,
|
|
19
75
|
max_retries: int = 3,
|
|
20
76
|
timeout: int = 5,
|
|
77
|
+
pre_login: bool = False,
|
|
21
78
|
) -> Optional[bool]:
|
|
22
79
|
"""
|
|
23
80
|
Detect UniFi OS with exponential backoff retry.
|
|
@@ -27,6 +84,8 @@ async def detect_with_retry(
|
|
|
27
84
|
base_url: Base URL of controller
|
|
28
85
|
max_retries: Maximum retry attempts (default: 3)
|
|
29
86
|
timeout: Detection timeout per attempt in seconds (default: 5)
|
|
87
|
+
pre_login: If True, use unauthenticated detection for auth endpoint selection.
|
|
88
|
+
If False, use authenticated detection for API path verification.
|
|
30
89
|
|
|
31
90
|
Returns:
|
|
32
91
|
True: UniFi OS detected
|
|
@@ -39,9 +98,11 @@ async def detect_with_retry(
|
|
|
39
98
|
- Logs retry attempts at debug level
|
|
40
99
|
- Returns None if all attempts fail
|
|
41
100
|
"""
|
|
101
|
+
detect_func = detect_unifi_os_pre_login if pre_login else detect_unifi_os_proactively
|
|
102
|
+
|
|
42
103
|
for attempt in range(max_retries):
|
|
43
104
|
try:
|
|
44
|
-
result = await
|
|
105
|
+
result = await detect_func(session, base_url, timeout)
|
|
45
106
|
if result is not None:
|
|
46
107
|
return result
|
|
47
108
|
except Exception as e:
|
|
@@ -55,22 +116,6 @@ async def detect_with_retry(
|
|
|
55
116
|
return None
|
|
56
117
|
|
|
57
118
|
|
|
58
|
-
def _generate_detection_failure_message(base_url: str, port: int) -> str:
|
|
59
|
-
"""Generate user-friendly troubleshooting message for detection failures."""
|
|
60
|
-
return f"""
|
|
61
|
-
UniFi controller path detection failed.
|
|
62
|
-
|
|
63
|
-
Troubleshooting Steps:
|
|
64
|
-
1. Verify network connectivity to {base_url}
|
|
65
|
-
2. Check controller is accessible on port {port}
|
|
66
|
-
3. Manually set controller type using environment variable:
|
|
67
|
-
- For UniFi OS (Cloud Gateway, UDM-Pro): export UNIFI_CONTROLLER_TYPE=proxy
|
|
68
|
-
- For standalone controllers: export UNIFI_CONTROLLER_TYPE=direct
|
|
69
|
-
|
|
70
|
-
For more help, see: https://github.com/sirkirby/unifi-network-mcp/issues/19
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
|
|
74
119
|
async def _probe_endpoint(
|
|
75
120
|
session: aiohttp.ClientSession,
|
|
76
121
|
url: str,
|
|
@@ -233,7 +278,11 @@ class ConnectionManager:
|
|
|
233
278
|
)
|
|
234
279
|
session_created = True
|
|
235
280
|
|
|
236
|
-
#
|
|
281
|
+
# Controller type detection/override configuration
|
|
282
|
+
# Two-phase detection:
|
|
283
|
+
# 1. Pre-login: Determines auth endpoint (/api/auth/login vs /api/login)
|
|
284
|
+
# 2. Post-login: Verifies API path prefix (/proxy/network/api vs /api)
|
|
285
|
+
# See: https://github.com/sirkirby/unifi-network-mcp/issues/33
|
|
237
286
|
from src.bootstrap import UNIFI_CONTROLLER_TYPE
|
|
238
287
|
|
|
239
288
|
if UNIFI_CONTROLLER_TYPE == "proxy":
|
|
@@ -242,6 +291,30 @@ class ConnectionManager:
|
|
|
242
291
|
elif UNIFI_CONTROLLER_TYPE == "direct":
|
|
243
292
|
self._unifi_os_override = False
|
|
244
293
|
logger.info("Controller type forced to standard (direct) via config")
|
|
294
|
+
elif UNIFI_CONTROLLER_TYPE == "auto":
|
|
295
|
+
# Phase 1: Pre-login detection (unauthenticated)
|
|
296
|
+
# Determines which auth endpoint to use
|
|
297
|
+
if self._unifi_os_override is None:
|
|
298
|
+
detected = await detect_with_retry(
|
|
299
|
+
self._aiohttp_session,
|
|
300
|
+
self.url_base,
|
|
301
|
+
max_retries=3,
|
|
302
|
+
timeout=5,
|
|
303
|
+
pre_login=True, # Use unauthenticated detection
|
|
304
|
+
)
|
|
305
|
+
if detected is not None:
|
|
306
|
+
self._unifi_os_override = detected
|
|
307
|
+
mode = "UniFi OS (proxy)" if detected else "standard (direct)"
|
|
308
|
+
logger.info(f"Pre-login auto-detected controller type: {mode}")
|
|
309
|
+
else:
|
|
310
|
+
# Pre-login detection inconclusive - aiounifi will try its own detection
|
|
311
|
+
# Show helpful message for troubleshooting
|
|
312
|
+
logger.warning(
|
|
313
|
+
"Pre-login detection inconclusive, deferring to aiounifi. "
|
|
314
|
+
"If login fails, try setting UNIFI_CONTROLLER_TYPE=proxy for UniFi OS devices."
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
logger.debug(f"Using cached detection result: {self._unifi_os_override}")
|
|
245
318
|
|
|
246
319
|
config = Configuration(
|
|
247
320
|
session=self._aiohttp_session,
|
|
@@ -254,30 +327,34 @@ class ConnectionManager:
|
|
|
254
327
|
|
|
255
328
|
self.controller = Controller(config=config)
|
|
256
329
|
|
|
330
|
+
# Apply pre-login detection result BEFORE login to ensure correct auth endpoint
|
|
331
|
+
# aiounifi uses /api/auth/login for UniFi OS, /api/login for standalone
|
|
332
|
+
if self._unifi_os_override is not None:
|
|
333
|
+
self.controller.connectivity.is_unifi_os = self._unifi_os_override
|
|
334
|
+
logger.debug(f"Pre-login is_unifi_os set to: {self._unifi_os_override}")
|
|
335
|
+
|
|
257
336
|
await self.controller.login()
|
|
258
337
|
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
338
|
+
# Phase 2: Post-login verification (authenticated)
|
|
339
|
+
# Verify API path prefix works correctly after successful login
|
|
340
|
+
if UNIFI_CONTROLLER_TYPE == "auto" and self._unifi_os_override is not None:
|
|
341
|
+
post_login_detected = await detect_with_retry(
|
|
342
|
+
self._aiohttp_session,
|
|
343
|
+
self.url_base,
|
|
344
|
+
max_retries=2,
|
|
345
|
+
timeout=5,
|
|
346
|
+
pre_login=False, # Use authenticated detection
|
|
347
|
+
)
|
|
348
|
+
if post_login_detected is not None and post_login_detected != self._unifi_os_override:
|
|
349
|
+
# Post-login detection differs - update override
|
|
350
|
+
logger.warning(
|
|
351
|
+
f"Post-login detection differs from pre-login: "
|
|
352
|
+
f"pre={self._unifi_os_override}, post={post_login_detected}. "
|
|
353
|
+
f"Using post-login result."
|
|
269
354
|
)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
logger.info(f"Auto-detected controller type: {mode}")
|
|
274
|
-
else:
|
|
275
|
-
# Show clear error message (FR-009)
|
|
276
|
-
error_msg = _generate_detection_failure_message(self.url_base, self.port)
|
|
277
|
-
logger.warning(error_msg)
|
|
278
|
-
logger.warning("Falling back to aiounifi's check_unifi_os()")
|
|
279
|
-
else:
|
|
280
|
-
logger.debug(f"Using cached detection result: {self._unifi_os_override}")
|
|
355
|
+
self._unifi_os_override = post_login_detected
|
|
356
|
+
elif post_login_detected is not None:
|
|
357
|
+
logger.debug("Post-login detection confirmed pre-login result")
|
|
281
358
|
|
|
282
359
|
self._initialized = True
|
|
283
360
|
logger.info(f"Successfully connected to Unifi controller at {self.host} for site '{self.site}'")
|
|
@@ -7,12 +7,63 @@ when first called by an LLM. This dramatically reduces initial context usage.
|
|
|
7
7
|
import importlib
|
|
8
8
|
import logging
|
|
9
9
|
from functools import wraps
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
from typing import Any, Callable, Dict, Set
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger("unifi-network-mcp")
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
def _build_tool_module_map() -> Dict[str, str]:
|
|
17
|
+
"""Build tool-to-module mapping by scanning tool files.
|
|
18
|
+
|
|
19
|
+
This dynamically discovers all tools and their modules, eliminating the need
|
|
20
|
+
for a manually-maintained static mapping that can get out of sync.
|
|
21
|
+
"""
|
|
22
|
+
tool_map: Dict[str, str] = {}
|
|
23
|
+
|
|
24
|
+
# Find the tools directory
|
|
25
|
+
# Try relative to this file first, then fall back to cwd
|
|
26
|
+
this_dir = Path(__file__).parent
|
|
27
|
+
tools_dir = this_dir.parent / "tools"
|
|
28
|
+
|
|
29
|
+
if not tools_dir.exists():
|
|
30
|
+
tools_dir = Path("src/tools")
|
|
31
|
+
|
|
32
|
+
if not tools_dir.exists():
|
|
33
|
+
logger.warning("Tools directory not found, falling back to static map")
|
|
34
|
+
return _STATIC_TOOL_MODULE_MAP
|
|
35
|
+
|
|
36
|
+
# Scan each .py file in tools directory
|
|
37
|
+
for tool_file in tools_dir.glob("*.py"):
|
|
38
|
+
if tool_file.name.startswith("_"):
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
module_name = f"src.tools.{tool_file.stem}"
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Read file and look for @server.tool or @permissioned_tool decorators
|
|
45
|
+
content = tool_file.read_text()
|
|
46
|
+
|
|
47
|
+
# Find tool names using simple pattern matching
|
|
48
|
+
# Looking for: name="unifi_xxx" or name='unifi_xxx'
|
|
49
|
+
import re
|
|
50
|
+
|
|
51
|
+
pattern = r'name\s*=\s*["\']([unifi_][a-z_]+)["\']'
|
|
52
|
+
matches = re.findall(pattern, content)
|
|
53
|
+
|
|
54
|
+
for tool_name in matches:
|
|
55
|
+
if tool_name.startswith("unifi_"):
|
|
56
|
+
tool_map[tool_name] = module_name
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.debug(f"Error scanning {tool_file}: {e}")
|
|
60
|
+
|
|
61
|
+
logger.debug(f"Built dynamic tool map with {len(tool_map)} tools")
|
|
62
|
+
return tool_map
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Static fallback map (used if dynamic discovery fails)
|
|
66
|
+
_STATIC_TOOL_MODULE_MAP: Dict[str, str] = {
|
|
16
67
|
# Client tools
|
|
17
68
|
"unifi_list_clients": "src.tools.clients",
|
|
18
69
|
"unifi_get_client_details": "src.tools.clients",
|
|
@@ -61,15 +112,21 @@ TOOL_MODULE_MAP: Dict[str, str] = {
|
|
|
61
112
|
"unifi_update_vpn_client_state": "src.tools.vpn",
|
|
62
113
|
# QoS tools
|
|
63
114
|
"unifi_list_qos_rules": "src.tools.qos",
|
|
115
|
+
"unifi_get_qos_rule_details": "src.tools.qos",
|
|
64
116
|
"unifi_create_qos_rule": "src.tools.qos",
|
|
117
|
+
"unifi_create_simple_qos_rule": "src.tools.qos",
|
|
65
118
|
"unifi_update_qos_rule": "src.tools.qos",
|
|
66
119
|
"unifi_delete_qos_rule": "src.tools.qos",
|
|
120
|
+
"unifi_toggle_qos_rule_enabled": "src.tools.qos",
|
|
67
121
|
# Statistics tools
|
|
68
122
|
"unifi_get_client_stats": "src.tools.stats",
|
|
69
123
|
"unifi_get_device_stats": "src.tools.stats",
|
|
70
124
|
"unifi_get_network_stats": "src.tools.stats",
|
|
71
125
|
"unifi_get_wireless_stats": "src.tools.stats",
|
|
72
126
|
"unifi_get_system_stats": "src.tools.stats",
|
|
127
|
+
"unifi_get_top_clients": "src.tools.stats",
|
|
128
|
+
"unifi_get_dpi_stats": "src.tools.stats",
|
|
129
|
+
"unifi_get_alerts": "src.tools.stats",
|
|
73
130
|
# System tools
|
|
74
131
|
"unifi_get_system_info": "src.tools.system",
|
|
75
132
|
"unifi_get_network_health": "src.tools.system",
|
|
@@ -101,8 +158,35 @@ TOOL_MODULE_MAP: Dict[str, str] = {
|
|
|
101
158
|
"unifi_get_traffic_route_details": "src.tools.traffic_routes",
|
|
102
159
|
"unifi_update_traffic_route": "src.tools.traffic_routes",
|
|
103
160
|
"unifi_toggle_traffic_route": "src.tools.traffic_routes",
|
|
161
|
+
# Port Forward tools
|
|
162
|
+
"unifi_list_port_forwards": "src.tools.port_forwards",
|
|
163
|
+
"unifi_get_port_forward": "src.tools.port_forwards",
|
|
164
|
+
"unifi_create_port_forward": "src.tools.port_forwards",
|
|
165
|
+
"unifi_create_simple_port_forward": "src.tools.port_forwards",
|
|
166
|
+
"unifi_update_port_forward": "src.tools.port_forwards",
|
|
167
|
+
"unifi_toggle_port_forward": "src.tools.port_forwards",
|
|
168
|
+
# Firewall Policy tools (zone-based)
|
|
169
|
+
"unifi_list_firewall_policies": "src.tools.firewall",
|
|
170
|
+
"unifi_list_firewall_zones": "src.tools.firewall",
|
|
171
|
+
"unifi_list_ip_groups": "src.tools.firewall",
|
|
172
|
+
"unifi_get_firewall_policy_details": "src.tools.firewall",
|
|
173
|
+
"unifi_create_firewall_policy": "src.tools.firewall",
|
|
174
|
+
"unifi_create_simple_firewall_policy": "src.tools.firewall",
|
|
175
|
+
"unifi_update_firewall_policy": "src.tools.firewall",
|
|
176
|
+
"unifi_toggle_firewall_policy": "src.tools.firewall",
|
|
177
|
+
# WLAN tools
|
|
178
|
+
"unifi_list_wlans": "src.tools.network",
|
|
179
|
+
"unifi_get_wlan_details": "src.tools.network",
|
|
180
|
+
"unifi_create_wlan": "src.tools.network",
|
|
181
|
+
"unifi_update_wlan": "src.tools.network",
|
|
182
|
+
# Device tools (additional)
|
|
183
|
+
"unifi_rename_device": "src.tools.devices",
|
|
104
184
|
}
|
|
105
185
|
|
|
186
|
+
# Build the tool map dynamically at module load time
|
|
187
|
+
# Falls back to static map if dynamic discovery fails
|
|
188
|
+
TOOL_MODULE_MAP: Dict[str, str] = _build_tool_module_map()
|
|
189
|
+
|
|
106
190
|
|
|
107
191
|
class LazyToolLoader:
|
|
108
192
|
"""Manages lazy/on-demand tool loading."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|