unifi-network-mcp 0.4.2__py3-none-any.whl → 0.5.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.
- src/_version.py +2 -2
- src/config/config.yaml +7 -3
- src/main.py +2 -1
- src/runtime.py +32 -1
- src/tools/clients.py +4 -4
- src/tools/system.py +95 -3
- src/tools_manifest.json +119 -1
- src/utils/lazy_tool_loader.py +41 -127
- src/utils/permissions.py +1 -0
- {unifi_network_mcp-0.4.2.dist-info → unifi_network_mcp-0.5.1.dist-info}/METADATA +3 -1
- {unifi_network_mcp-0.4.2.dist-info → unifi_network_mcp-0.5.1.dist-info}/RECORD +15 -15
- {unifi_network_mcp-0.4.2.data → unifi_network_mcp-0.5.1.data}/data/share/unifi-network-mcp/.well-known/mcp-server.json +0 -0
- {unifi_network_mcp-0.4.2.dist-info → unifi_network_mcp-0.5.1.dist-info}/WHEEL +0 -0
- {unifi_network_mcp-0.4.2.dist-info → unifi_network_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {unifi_network_mcp-0.4.2.dist-info → unifi_network_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
src/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.5.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
src/config/config.yaml
CHANGED
|
@@ -8,8 +8,8 @@ unifi:
|
|
|
8
8
|
controller_type: ${oc.env:UNIFI_CONTROLLER_TYPE,auto}
|
|
9
9
|
|
|
10
10
|
server:
|
|
11
|
-
host: 0.0.0.0
|
|
12
|
-
port: 3000
|
|
11
|
+
host: ${oc.env:UNIFI_MCP_HOST,0.0.0.0}
|
|
12
|
+
port: ${oc.env:UNIFI_MCP_PORT,3000}
|
|
13
13
|
log_level: INFO
|
|
14
14
|
|
|
15
15
|
# Tool registration mode
|
|
@@ -118,4 +118,8 @@ permissions:
|
|
|
118
118
|
|
|
119
119
|
routes:
|
|
120
120
|
create: false # Static routes can disrupt connectivity
|
|
121
|
-
update: false # Modifying routes requires careful planning
|
|
121
|
+
update: false # Modifying routes requires careful planning
|
|
122
|
+
|
|
123
|
+
snmp:
|
|
124
|
+
create: false # Not applicable
|
|
125
|
+
update: true # Allow enabling/disabling SNMP and changing community
|
src/main.py
CHANGED
|
@@ -35,7 +35,8 @@ from src.utils.meta_tools import register_load_tools, register_meta_tools
|
|
|
35
35
|
from src.utils.permissions import parse_permission # noqa: E402
|
|
36
36
|
from src.utils.tool_loader import auto_load_tools
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
# Use the original FastMCP tool decorator (saved in runtime.py before wrapping)
|
|
39
|
+
_original_tool_decorator = getattr(server, "_original_tool", server.tool)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def permissioned_tool(*d_args, **d_kwargs): # acts like @server.tool
|
src/runtime.py
CHANGED
|
@@ -12,6 +12,10 @@ Downstream code (tool modules, tests, etc.) should import these via::
|
|
|
12
12
|
|
|
13
13
|
Lazy factories (`get_*`) are provided so unit tests can substitute fakes by
|
|
14
14
|
monkey‑patching before the first call.
|
|
15
|
+
|
|
16
|
+
IMPORTANT: The server's `tool` decorator is wrapped here (not in main.py) to
|
|
17
|
+
ensure that tool modules can be imported directly (for testing, etc.) without
|
|
18
|
+
errors from unrecognized decorator kwargs like `permission_category`.
|
|
15
19
|
"""
|
|
16
20
|
|
|
17
21
|
import os
|
|
@@ -49,6 +53,25 @@ def get_config():
|
|
|
49
53
|
return load_config()
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
def _create_permissioned_tool_wrapper(original_tool_decorator):
|
|
57
|
+
"""Wrap the FastMCP tool decorator to handle permission kwargs.
|
|
58
|
+
|
|
59
|
+
This wrapper strips `permission_category` and `permission_action` kwargs
|
|
60
|
+
before passing to the original FastMCP decorator. This allows tool modules
|
|
61
|
+
to be imported directly (for testing, etc.) without errors.
|
|
62
|
+
|
|
63
|
+
The actual permission checking is done in main.py's permissioned_tool,
|
|
64
|
+
which replaces this wrapper at startup. This wrapper just ensures imports
|
|
65
|
+
don't fail when tools have permission kwargs.
|
|
66
|
+
"""
|
|
67
|
+
def wrapper(*args, **kwargs):
|
|
68
|
+
# Strip permission-related kwargs that FastMCP doesn't understand
|
|
69
|
+
kwargs.pop("permission_category", None)
|
|
70
|
+
kwargs.pop("permission_action", None)
|
|
71
|
+
return original_tool_decorator(*args, **kwargs)
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
52
75
|
@lru_cache
|
|
53
76
|
def get_server() -> FastMCP:
|
|
54
77
|
"""Create the FastMCP server instance exactly once."""
|
|
@@ -62,12 +85,20 @@ def get_server() -> FastMCP:
|
|
|
62
85
|
|
|
63
86
|
logger.debug(f"Configuring FastMCP with allowed_hosts: {allowed_hosts}")
|
|
64
87
|
|
|
65
|
-
|
|
88
|
+
server = FastMCP(
|
|
66
89
|
name="unifi-network-mcp",
|
|
67
90
|
debug=True,
|
|
68
91
|
transport_security=transport_security,
|
|
69
92
|
)
|
|
70
93
|
|
|
94
|
+
# Wrap the tool decorator to handle permission kwargs gracefully.
|
|
95
|
+
# This ensures tool modules can be imported directly without errors.
|
|
96
|
+
# main.py will replace this with the full permissioned_tool implementation.
|
|
97
|
+
server._original_tool = server.tool
|
|
98
|
+
server.tool = _create_permissioned_tool_wrapper(server.tool)
|
|
99
|
+
|
|
100
|
+
return server
|
|
101
|
+
|
|
71
102
|
|
|
72
103
|
# ---------------------------------------------------------------------------
|
|
73
104
|
# Manager factories ---------------------------------------------------------
|
src/tools/clients.py
CHANGED
|
@@ -485,10 +485,10 @@ Either setting can be enabled/disabled independently.""",
|
|
|
485
485
|
)
|
|
486
486
|
async def set_client_ip_settings(
|
|
487
487
|
mac_address: str,
|
|
488
|
-
use_fixedip:
|
|
489
|
-
fixed_ip:
|
|
490
|
-
local_dns_record_enabled:
|
|
491
|
-
local_dns_record:
|
|
488
|
+
use_fixedip: bool | None = None,
|
|
489
|
+
fixed_ip: str | None = None,
|
|
490
|
+
local_dns_record_enabled: bool | None = None,
|
|
491
|
+
local_dns_record: str | None = None,
|
|
492
492
|
confirm: bool = False,
|
|
493
493
|
) -> Dict[str, Any]:
|
|
494
494
|
"""Set fixed IP and/or local DNS record for a client."""
|
src/tools/system.py
CHANGED
|
@@ -5,9 +5,11 @@ This module provides MCP tools to interact with a Unifi Network Controller's sys
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any, Dict
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
9
|
|
|
10
|
-
from src.runtime import server, system_manager
|
|
10
|
+
from src.runtime import config, server, system_manager
|
|
11
|
+
from src.utils.confirmation import should_auto_confirm, update_preview
|
|
12
|
+
from src.utils.permissions import parse_permission
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
@@ -72,5 +74,95 @@ async def get_site_settings() -> Dict[str, Any]:
|
|
|
72
74
|
return {"success": False, "error": str(e)}
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
@server.tool(
|
|
78
|
+
name="unifi_get_snmp_settings",
|
|
79
|
+
description="Get current SNMP settings for the site (enabled state, community string).",
|
|
80
|
+
)
|
|
81
|
+
async def get_snmp_settings() -> Dict[str, Any]:
|
|
82
|
+
"""Implementation for getting SNMP settings."""
|
|
83
|
+
logger.info("unifi_get_snmp_settings tool called")
|
|
84
|
+
try:
|
|
85
|
+
settings_list = await system_manager.get_settings("snmp")
|
|
86
|
+
snmp_settings = settings_list[0] if settings_list else {}
|
|
87
|
+
return {
|
|
88
|
+
"success": True,
|
|
89
|
+
"site": system_manager._connection.site,
|
|
90
|
+
"snmp_settings": {
|
|
91
|
+
"enabled": snmp_settings.get("enabled", False),
|
|
92
|
+
"community": snmp_settings.get("community", ""),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Error getting SNMP settings: {e}", exc_info=True)
|
|
97
|
+
return {"success": False, "error": str(e)}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@server.tool(
|
|
101
|
+
name="unifi_update_snmp_settings",
|
|
102
|
+
description="Update SNMP settings for the site (enable/disable, set community string). Requires confirm=true to apply changes.",
|
|
103
|
+
)
|
|
104
|
+
async def update_snmp_settings(
|
|
105
|
+
enabled: bool,
|
|
106
|
+
community: Optional[str] = None,
|
|
107
|
+
confirm: bool = False,
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""Implementation for updating SNMP settings.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
enabled: Whether SNMP should be enabled on the site.
|
|
113
|
+
community: SNMP community string (optional, keeps current value if not provided).
|
|
114
|
+
confirm: Must be true to apply changes. When false, returns a preview of proposed changes.
|
|
115
|
+
"""
|
|
116
|
+
logger.info(f"unifi_update_snmp_settings tool called (enabled={enabled}, confirm={confirm})")
|
|
117
|
+
|
|
118
|
+
if not parse_permission(config.permissions, "snmp", "update"):
|
|
119
|
+
logger.warning("Permission denied for updating SNMP settings.")
|
|
120
|
+
return {"success": False, "error": "Permission denied to update SNMP settings."}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
settings_list = await system_manager.get_settings("snmp")
|
|
124
|
+
current = settings_list[0] if settings_list else {}
|
|
125
|
+
|
|
126
|
+
updates: Dict[str, Any] = {"enabled": enabled}
|
|
127
|
+
if community is not None:
|
|
128
|
+
updates["community"] = community
|
|
129
|
+
|
|
130
|
+
if not confirm and not should_auto_confirm():
|
|
131
|
+
return update_preview(
|
|
132
|
+
resource_type="snmp_settings",
|
|
133
|
+
resource_id=current.get("_id", "snmp"),
|
|
134
|
+
resource_name="SNMP Settings",
|
|
135
|
+
current_state={
|
|
136
|
+
"enabled": current.get("enabled", False),
|
|
137
|
+
"community": current.get("community", ""),
|
|
138
|
+
},
|
|
139
|
+
updates=updates,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
payload: Dict[str, Any] = {"enabled": enabled}
|
|
143
|
+
if community is not None:
|
|
144
|
+
payload["community"] = community
|
|
145
|
+
|
|
146
|
+
success = await system_manager.update_settings("snmp", payload)
|
|
147
|
+
if success:
|
|
148
|
+
refreshed = await system_manager.get_settings("snmp")
|
|
149
|
+
new_settings = refreshed[0] if refreshed else payload
|
|
150
|
+
return {
|
|
151
|
+
"success": True,
|
|
152
|
+
"site": system_manager._connection.site,
|
|
153
|
+
"snmp_settings": {
|
|
154
|
+
"enabled": new_settings.get("enabled", enabled),
|
|
155
|
+
"community": new_settings.get("community", community or ""),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
return {"success": False, "error": "Failed to update SNMP settings."}
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error updating SNMP settings: {e}", exc_info=True)
|
|
161
|
+
return {"success": False, "error": str(e)}
|
|
162
|
+
|
|
163
|
+
|
|
75
164
|
# Print confirmation that all tools have been registered
|
|
76
|
-
logger.info(
|
|
165
|
+
logger.info(
|
|
166
|
+
"System tools registered: unifi_get_system_info, unifi_get_network_health, "
|
|
167
|
+
"unifi_get_site_settings, unifi_get_snmp_settings, unifi_update_snmp_settings"
|
|
168
|
+
)
|
src/tools_manifest.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
{
|
|
2
|
-
"count":
|
|
2
|
+
"count": 83,
|
|
3
3
|
"generated_by": "scripts/generate_tool_manifest.py",
|
|
4
|
+
"module_map": {
|
|
5
|
+
"unifi_adopt_device": "src.tools.devices",
|
|
6
|
+
"unifi_archive_alarm": "src.tools.events",
|
|
7
|
+
"unifi_archive_all_alarms": "src.tools.events",
|
|
8
|
+
"unifi_authorize_guest": "src.tools.clients",
|
|
9
|
+
"unifi_block_client": "src.tools.clients",
|
|
10
|
+
"unifi_create_firewall_policy": "src.tools.firewall",
|
|
11
|
+
"unifi_create_network": "src.tools.network",
|
|
12
|
+
"unifi_create_port_forward": "src.tools.port_forwards",
|
|
13
|
+
"unifi_create_qos_rule": "src.tools.qos",
|
|
14
|
+
"unifi_create_route": "src.tools.routing",
|
|
15
|
+
"unifi_create_simple_firewall_policy": "src.tools.firewall",
|
|
16
|
+
"unifi_create_simple_port_forward": "src.tools.port_forwards",
|
|
17
|
+
"unifi_create_simple_qos_rule": "src.tools.qos",
|
|
18
|
+
"unifi_create_usergroup": "src.tools.usergroups",
|
|
19
|
+
"unifi_create_voucher": "src.tools.hotspot",
|
|
20
|
+
"unifi_create_wlan": "src.tools.network",
|
|
21
|
+
"unifi_force_reconnect_client": "src.tools.clients",
|
|
22
|
+
"unifi_get_alerts": "src.tools.stats",
|
|
23
|
+
"unifi_get_client_details": "src.tools.clients",
|
|
24
|
+
"unifi_get_client_stats": "src.tools.stats",
|
|
25
|
+
"unifi_get_device_details": "src.tools.devices",
|
|
26
|
+
"unifi_get_device_stats": "src.tools.stats",
|
|
27
|
+
"unifi_get_dpi_stats": "src.tools.stats",
|
|
28
|
+
"unifi_get_event_types": "src.tools.events",
|
|
29
|
+
"unifi_get_firewall_policy_details": "src.tools.firewall",
|
|
30
|
+
"unifi_get_network_details": "src.tools.network",
|
|
31
|
+
"unifi_get_network_health": "src.tools.system",
|
|
32
|
+
"unifi_get_network_stats": "src.tools.stats",
|
|
33
|
+
"unifi_get_port_forward": "src.tools.port_forwards",
|
|
34
|
+
"unifi_get_qos_rule_details": "src.tools.qos",
|
|
35
|
+
"unifi_get_route_details": "src.tools.routing",
|
|
36
|
+
"unifi_get_site_settings": "src.tools.config",
|
|
37
|
+
"unifi_get_snmp_settings": "src.tools.system",
|
|
38
|
+
"unifi_get_system_info": "src.tools.system",
|
|
39
|
+
"unifi_get_top_clients": "src.tools.stats",
|
|
40
|
+
"unifi_get_traffic_route_details": "src.tools.traffic_routes",
|
|
41
|
+
"unifi_get_usergroup_details": "src.tools.usergroups",
|
|
42
|
+
"unifi_get_voucher_details": "src.tools.hotspot",
|
|
43
|
+
"unifi_get_vpn_client_details": "src.tools.vpn",
|
|
44
|
+
"unifi_get_vpn_server_details": "src.tools.vpn",
|
|
45
|
+
"unifi_get_wlan_details": "src.tools.network",
|
|
46
|
+
"unifi_list_active_routes": "src.tools.routing",
|
|
47
|
+
"unifi_list_alarms": "src.tools.events",
|
|
48
|
+
"unifi_list_blocked_clients": "src.tools.clients",
|
|
49
|
+
"unifi_list_clients": "src.tools.clients",
|
|
50
|
+
"unifi_list_devices": "src.tools.devices",
|
|
51
|
+
"unifi_list_events": "src.tools.events",
|
|
52
|
+
"unifi_list_firewall_policies": "src.tools.firewall",
|
|
53
|
+
"unifi_list_firewall_zones": "src.tools.firewall",
|
|
54
|
+
"unifi_list_ip_groups": "src.tools.firewall",
|
|
55
|
+
"unifi_list_networks": "src.tools.network",
|
|
56
|
+
"unifi_list_port_forwards": "src.tools.port_forwards",
|
|
57
|
+
"unifi_list_qos_rules": "src.tools.qos",
|
|
58
|
+
"unifi_list_routes": "src.tools.routing",
|
|
59
|
+
"unifi_list_traffic_routes": "src.tools.traffic_routes",
|
|
60
|
+
"unifi_list_usergroups": "src.tools.usergroups",
|
|
61
|
+
"unifi_list_vouchers": "src.tools.hotspot",
|
|
62
|
+
"unifi_list_vpn_clients": "src.tools.vpn",
|
|
63
|
+
"unifi_list_vpn_servers": "src.tools.vpn",
|
|
64
|
+
"unifi_list_wlans": "src.tools.network",
|
|
65
|
+
"unifi_reboot_device": "src.tools.devices",
|
|
66
|
+
"unifi_rename_client": "src.tools.clients",
|
|
67
|
+
"unifi_rename_device": "src.tools.devices",
|
|
68
|
+
"unifi_revoke_voucher": "src.tools.hotspot",
|
|
69
|
+
"unifi_set_client_ip_settings": "src.tools.clients",
|
|
70
|
+
"unifi_toggle_firewall_policy": "src.tools.firewall",
|
|
71
|
+
"unifi_toggle_port_forward": "src.tools.port_forwards",
|
|
72
|
+
"unifi_toggle_qos_rule_enabled": "src.tools.qos",
|
|
73
|
+
"unifi_toggle_traffic_route": "src.tools.traffic_routes",
|
|
74
|
+
"unifi_unauthorize_guest": "src.tools.clients",
|
|
75
|
+
"unifi_unblock_client": "src.tools.clients",
|
|
76
|
+
"unifi_update_firewall_policy": "src.tools.firewall",
|
|
77
|
+
"unifi_update_network": "src.tools.network",
|
|
78
|
+
"unifi_update_port_forward": "src.tools.port_forwards",
|
|
79
|
+
"unifi_update_qos_rule": "src.tools.qos",
|
|
80
|
+
"unifi_update_route": "src.tools.routing",
|
|
81
|
+
"unifi_update_snmp_settings": "src.tools.system",
|
|
82
|
+
"unifi_update_traffic_route": "src.tools.traffic_routes",
|
|
83
|
+
"unifi_update_usergroup": "src.tools.usergroups",
|
|
84
|
+
"unifi_update_vpn_client_state": "src.tools.vpn",
|
|
85
|
+
"unifi_update_vpn_server_state": "src.tools.vpn",
|
|
86
|
+
"unifi_update_wlan": "src.tools.network",
|
|
87
|
+
"unifi_upgrade_device": "src.tools.devices"
|
|
88
|
+
},
|
|
4
89
|
"note": "Auto-generated with full schemas from tool decorators. Do not edit manually.",
|
|
5
90
|
"tools": [
|
|
6
91
|
{
|
|
@@ -611,6 +696,16 @@
|
|
|
611
696
|
}
|
|
612
697
|
}
|
|
613
698
|
},
|
|
699
|
+
{
|
|
700
|
+
"description": "Get current SNMP settings for the site (enabled state, community string).",
|
|
701
|
+
"name": "unifi_get_snmp_settings",
|
|
702
|
+
"schema": {
|
|
703
|
+
"input": {
|
|
704
|
+
"properties": {},
|
|
705
|
+
"type": "object"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
614
709
|
{
|
|
615
710
|
"description": "Get general system information from the Unifi Network controller (version, uptime, etc).",
|
|
616
711
|
"name": "unifi_get_system_info",
|
|
@@ -1345,6 +1440,29 @@
|
|
|
1345
1440
|
}
|
|
1346
1441
|
}
|
|
1347
1442
|
},
|
|
1443
|
+
{
|
|
1444
|
+
"description": "Update SNMP settings for the site (enable/disable, set community string). Requires confirm=true to apply changes.",
|
|
1445
|
+
"name": "unifi_update_snmp_settings",
|
|
1446
|
+
"schema": {
|
|
1447
|
+
"input": {
|
|
1448
|
+
"properties": {
|
|
1449
|
+
"community": {
|
|
1450
|
+
"type": "string"
|
|
1451
|
+
},
|
|
1452
|
+
"confirm": {
|
|
1453
|
+
"type": "boolean"
|
|
1454
|
+
},
|
|
1455
|
+
"enabled": {
|
|
1456
|
+
"type": "boolean"
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
"required": [
|
|
1460
|
+
"enabled"
|
|
1461
|
+
],
|
|
1462
|
+
"type": "object"
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1348
1466
|
{
|
|
1349
1467
|
"description": "Update a traffic route's settings.\n\nCan update:\n- enabled: Enable or disable the traffic route\n- kill_switch_enabled: Enable/disable the kill switch (blocks traffic if VPN is down)\n\nAt least one parameter must be provided.",
|
|
1350
1468
|
"name": "unifi_update_traffic_route",
|
src/utils/lazy_tool_loader.py
CHANGED
|
@@ -5,7 +5,9 @@ when first called by an LLM. This dramatically reduces initial context usage.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import importlib
|
|
8
|
+
import json
|
|
8
9
|
import logging
|
|
10
|
+
import re
|
|
9
11
|
from functools import wraps
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Any, Callable, Dict, Set
|
|
@@ -13,11 +15,45 @@ from typing import Any, Callable, Dict, Set
|
|
|
13
15
|
logger = logging.getLogger("unifi-network-mcp")
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
def _load_module_map_from_manifest() -> Dict[str, str]:
|
|
19
|
+
"""Load tool-to-module mapping from the manifest file.
|
|
20
|
+
|
|
21
|
+
The manifest is auto-generated by scripts/generate_tool_manifest.py and
|
|
22
|
+
includes a module_map that stays in sync with the actual tools.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dictionary mapping tool names to their module paths, or empty dict if unavailable
|
|
26
|
+
"""
|
|
27
|
+
# Try relative to this file first
|
|
28
|
+
manifest_path = Path(__file__).parent.parent / "tools_manifest.json"
|
|
29
|
+
|
|
30
|
+
if not manifest_path.exists():
|
|
31
|
+
# Try relative to cwd
|
|
32
|
+
manifest_path = Path("src/tools_manifest.json")
|
|
33
|
+
|
|
34
|
+
if not manifest_path.exists():
|
|
35
|
+
logger.warning("Tools manifest not found for fallback loading")
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with open(manifest_path) as f:
|
|
40
|
+
manifest = json.load(f)
|
|
41
|
+
module_map = manifest.get("module_map", {})
|
|
42
|
+
logger.info(f"Loaded module map from manifest with {len(module_map)} tools")
|
|
43
|
+
return module_map
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.warning(f"Failed to load module map from manifest: {e}")
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
16
49
|
def _build_tool_module_map() -> Dict[str, str]:
|
|
17
50
|
"""Build tool-to-module mapping by scanning tool files.
|
|
18
51
|
|
|
19
52
|
This dynamically discovers all tools and their modules, eliminating the need
|
|
20
53
|
for a manually-maintained static mapping that can get out of sync.
|
|
54
|
+
|
|
55
|
+
Falls back to loading from tools_manifest.json if the tools directory
|
|
56
|
+
is not found (e.g., in unusual deployment scenarios).
|
|
21
57
|
"""
|
|
22
58
|
tool_map: Dict[str, str] = {}
|
|
23
59
|
|
|
@@ -30,8 +66,8 @@ def _build_tool_module_map() -> Dict[str, str]:
|
|
|
30
66
|
tools_dir = Path("src/tools")
|
|
31
67
|
|
|
32
68
|
if not tools_dir.exists():
|
|
33
|
-
logger.warning("Tools directory not found, falling back to
|
|
34
|
-
return
|
|
69
|
+
logger.warning("Tools directory not found, falling back to manifest")
|
|
70
|
+
return _load_module_map_from_manifest()
|
|
35
71
|
|
|
36
72
|
# Scan each .py file in tools directory
|
|
37
73
|
for tool_file in tools_dir.glob("*.py"):
|
|
@@ -46,9 +82,8 @@ def _build_tool_module_map() -> Dict[str, str]:
|
|
|
46
82
|
|
|
47
83
|
# Find tool names using simple pattern matching
|
|
48
84
|
# Looking for: name="unifi_xxx" or name='unifi_xxx'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
pattern = r'name\s*=\s*["\']([unifi_][a-z_]+)["\']'
|
|
85
|
+
# Note: pattern uses literal 'unifi_' prefix, not a character class
|
|
86
|
+
pattern = r'name\s*=\s*["\'](unifi_[a-z_]+)["\']'
|
|
52
87
|
matches = re.findall(pattern, content)
|
|
53
88
|
|
|
54
89
|
for tool_name in matches:
|
|
@@ -62,129 +97,8 @@ def _build_tool_module_map() -> Dict[str, str]:
|
|
|
62
97
|
return tool_map
|
|
63
98
|
|
|
64
99
|
|
|
65
|
-
# Static fallback map (used if dynamic discovery fails)
|
|
66
|
-
_STATIC_TOOL_MODULE_MAP: Dict[str, str] = {
|
|
67
|
-
# Client tools
|
|
68
|
-
"unifi_list_clients": "src.tools.clients",
|
|
69
|
-
"unifi_get_client_details": "src.tools.clients",
|
|
70
|
-
"unifi_list_blocked_clients": "src.tools.clients",
|
|
71
|
-
"unifi_block_client": "src.tools.clients",
|
|
72
|
-
"unifi_unblock_client": "src.tools.clients",
|
|
73
|
-
"unifi_rename_client": "src.tools.clients",
|
|
74
|
-
"unifi_force_reconnect_client": "src.tools.clients",
|
|
75
|
-
"unifi_authorize_guest": "src.tools.clients",
|
|
76
|
-
"unifi_unauthorize_guest": "src.tools.clients",
|
|
77
|
-
"unifi_set_client_ip_settings": "src.tools.clients",
|
|
78
|
-
# Device tools
|
|
79
|
-
"unifi_list_devices": "src.tools.devices",
|
|
80
|
-
"unifi_get_device_details": "src.tools.devices",
|
|
81
|
-
"unifi_reboot_device": "src.tools.devices",
|
|
82
|
-
"unifi_locate_device": "src.tools.devices",
|
|
83
|
-
"unifi_adopt_device": "src.tools.devices",
|
|
84
|
-
"unifi_upgrade_device": "src.tools.devices",
|
|
85
|
-
"unifi_set_device_name": "src.tools.devices",
|
|
86
|
-
"unifi_power_cycle_port": "src.tools.devices",
|
|
87
|
-
"unifi_set_port_profile": "src.tools.devices",
|
|
88
|
-
# Network tools
|
|
89
|
-
"unifi_list_networks": "src.tools.networks",
|
|
90
|
-
"unifi_get_network_details": "src.tools.networks",
|
|
91
|
-
"unifi_create_network": "src.tools.networks",
|
|
92
|
-
"unifi_update_network": "src.tools.networks",
|
|
93
|
-
"unifi_delete_network": "src.tools.networks",
|
|
94
|
-
# Firewall tools
|
|
95
|
-
"unifi_list_firewall_rules": "src.tools.firewall",
|
|
96
|
-
"unifi_get_firewall_rule": "src.tools.firewall",
|
|
97
|
-
"unifi_create_firewall_rule": "src.tools.firewall",
|
|
98
|
-
"unifi_update_firewall_rule": "src.tools.firewall",
|
|
99
|
-
"unifi_delete_firewall_rule": "src.tools.firewall",
|
|
100
|
-
"unifi_enable_firewall_rule": "src.tools.firewall",
|
|
101
|
-
"unifi_disable_firewall_rule": "src.tools.firewall",
|
|
102
|
-
"unifi_list_firewall_groups": "src.tools.firewall",
|
|
103
|
-
"unifi_create_firewall_group": "src.tools.firewall",
|
|
104
|
-
"unifi_update_firewall_group": "src.tools.firewall",
|
|
105
|
-
"unifi_delete_firewall_group": "src.tools.firewall",
|
|
106
|
-
# VPN tools
|
|
107
|
-
"unifi_list_vpn_servers": "src.tools.vpn",
|
|
108
|
-
"unifi_get_vpn_server_details": "src.tools.vpn",
|
|
109
|
-
"unifi_update_vpn_server_state": "src.tools.vpn",
|
|
110
|
-
"unifi_list_vpn_clients": "src.tools.vpn",
|
|
111
|
-
"unifi_get_vpn_client_details": "src.tools.vpn",
|
|
112
|
-
"unifi_update_vpn_client_state": "src.tools.vpn",
|
|
113
|
-
# QoS tools
|
|
114
|
-
"unifi_list_qos_rules": "src.tools.qos",
|
|
115
|
-
"unifi_get_qos_rule_details": "src.tools.qos",
|
|
116
|
-
"unifi_create_qos_rule": "src.tools.qos",
|
|
117
|
-
"unifi_create_simple_qos_rule": "src.tools.qos",
|
|
118
|
-
"unifi_update_qos_rule": "src.tools.qos",
|
|
119
|
-
"unifi_delete_qos_rule": "src.tools.qos",
|
|
120
|
-
"unifi_toggle_qos_rule_enabled": "src.tools.qos",
|
|
121
|
-
# Statistics tools
|
|
122
|
-
"unifi_get_client_stats": "src.tools.stats",
|
|
123
|
-
"unifi_get_device_stats": "src.tools.stats",
|
|
124
|
-
"unifi_get_network_stats": "src.tools.stats",
|
|
125
|
-
"unifi_get_wireless_stats": "src.tools.stats",
|
|
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",
|
|
130
|
-
# System tools
|
|
131
|
-
"unifi_get_system_info": "src.tools.system",
|
|
132
|
-
"unifi_get_network_health": "src.tools.system",
|
|
133
|
-
"unifi_get_site_settings": "src.tools.system",
|
|
134
|
-
# Event tools
|
|
135
|
-
"unifi_list_events": "src.tools.events",
|
|
136
|
-
"unifi_list_alarms": "src.tools.events",
|
|
137
|
-
"unifi_get_event_types": "src.tools.events",
|
|
138
|
-
"unifi_archive_alarm": "src.tools.events",
|
|
139
|
-
"unifi_archive_all_alarms": "src.tools.events",
|
|
140
|
-
# Hotspot/Voucher tools
|
|
141
|
-
"unifi_list_vouchers": "src.tools.hotspot",
|
|
142
|
-
"unifi_get_voucher_details": "src.tools.hotspot",
|
|
143
|
-
"unifi_create_voucher": "src.tools.hotspot",
|
|
144
|
-
"unifi_revoke_voucher": "src.tools.hotspot",
|
|
145
|
-
# User group tools
|
|
146
|
-
"unifi_list_usergroups": "src.tools.usergroups",
|
|
147
|
-
"unifi_get_usergroup_details": "src.tools.usergroups",
|
|
148
|
-
"unifi_create_usergroup": "src.tools.usergroups",
|
|
149
|
-
"unifi_update_usergroup": "src.tools.usergroups",
|
|
150
|
-
# Static Routing tools (V1 API)
|
|
151
|
-
"unifi_list_routes": "src.tools.routing",
|
|
152
|
-
"unifi_list_active_routes": "src.tools.routing",
|
|
153
|
-
"unifi_get_route_details": "src.tools.routing",
|
|
154
|
-
"unifi_create_route": "src.tools.routing",
|
|
155
|
-
"unifi_update_route": "src.tools.routing",
|
|
156
|
-
# Traffic Route tools (V2 API - policy-based routing)
|
|
157
|
-
"unifi_list_traffic_routes": "src.tools.traffic_routes",
|
|
158
|
-
"unifi_get_traffic_route_details": "src.tools.traffic_routes",
|
|
159
|
-
"unifi_update_traffic_route": "src.tools.traffic_routes",
|
|
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",
|
|
184
|
-
}
|
|
185
|
-
|
|
186
100
|
# Build the tool map dynamically at module load time
|
|
187
|
-
# Falls back to
|
|
101
|
+
# Falls back to manifest if dynamic discovery fails
|
|
188
102
|
TOOL_MODULE_MAP: Dict[str, str] = _build_tool_module_map()
|
|
189
103
|
|
|
190
104
|
|
src/utils/permissions.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Unifi Network MCP Server
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -534,6 +534,8 @@ The server merges settings from **environment variables**, an optional `.env` fi
|
|
|
534
534
|
| `UNIFI_VERIFY_SSL` | Set to `false` if using self-signed certs |
|
|
535
535
|
| `UNIFI_CONTROLLER_TYPE` | Controller API path type: `auto` (detect), `proxy` (UniFi OS), `direct` (standalone). Default `auto` |
|
|
536
536
|
| `UNIFI_MCP_HTTP_ENABLED` | Set `true` to enable optional HTTP SSE server (default `false`) |
|
|
537
|
+
| `UNIFI_MCP_HOST` | HTTP SSE bind address (default `0.0.0.0`) |
|
|
538
|
+
| `UNIFI_MCP_PORT` | HTTP SSE bind port (default `3000`) |
|
|
537
539
|
| `UNIFI_AUTO_CONFIRM` | Set `true` to auto-confirm all mutating operations (skips preview step). Ideal for workflow automation (n8n, Make, Zapier). Default `false` |
|
|
538
540
|
| `UNIFI_TOOL_REGISTRATION_MODE` | Tool loading mode: `lazy` (default), `eager`, or `meta_only`. See [Context Optimization](#context-optimization) |
|
|
539
541
|
| `UNIFI_ENABLED_CATEGORIES` | Comma-separated list of tool categories to load (eager mode). See table below |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
src/_version.py,sha256=
|
|
1
|
+
src/_version.py,sha256=cYMOhuaBHd0MIZmumuccsEQ-AxM8LIJy9dsBAWgOpqE,704
|
|
2
2
|
src/bootstrap.py,sha256=mqkDDfn5xKRzun07TS8xPLZm_WnfLt3xTguxxFnfRV0,7572
|
|
3
3
|
src/jobs.py,sha256=3IUO8ChpeRUjyJTWeFlyIwtkQLv-_KvfzuA5Ra4PmDI,5984
|
|
4
|
-
src/main.py,sha256=
|
|
5
|
-
src/runtime.py,sha256=
|
|
4
|
+
src/main.py,sha256=gmOt3jlpWv5r9dlIpva7YCkZ8Yw45jQR3k9o7hBQhw4,15830
|
|
5
|
+
src/runtime.py,sha256=Johuv7cVP0SFD9zDgjJt52E-DvwtcLZpH8FlzP1caFQ,7236
|
|
6
6
|
src/schemas.py,sha256=Y9fglAAgRGh8zxwO0J5zpj5BsRMLDDwevwXY_6kj5VU,33725
|
|
7
7
|
src/tool_index.py,sha256=aUPFqES4i_cq9ZsfAoiWuhKwoEEJ8NEAOafQ2h1g1NE,5492
|
|
8
8
|
src/validator_registry.py,sha256=dboNY36ZQphCr7tDQuVQtNvXu4X1FBkgqoLb-sGNwJQ,3005
|
|
@@ -21,7 +21,7 @@ src/managers/system_manager.py,sha256=xiobpRRQLEZ_e4aLpWuvcUcr6izMUGCdB5uH6o88_y
|
|
|
21
21
|
src/managers/traffic_route_manager.py,sha256=S0aMXAQD0ehAwbNVYsisIQbpGY5ubWTgOd1aCHChKhg,6517
|
|
22
22
|
src/managers/usergroup_manager.py,sha256=Zgz6IOc9HgCX9IbgJ1YVdG87I8SslJq-8WhITCWDR5A,6289
|
|
23
23
|
src/managers/vpn_manager.py,sha256=7uwKvRlM8LFcExjeF4Vs2bPODhMq3KeLcgkIAy7dSJM,11117
|
|
24
|
-
src/tools/clients.py,sha256=
|
|
24
|
+
src/tools/clients.py,sha256=gON4vrvMTguQ5eWlADGZpTjxC8RASTfTo32h-JQuP4Q,23112
|
|
25
25
|
src/tools/config.py,sha256=8sgZTZmm3H0sBRgbmuOoyHCps6z6O-W-Hd9WnkeDJ94,1741
|
|
26
26
|
src/tools/devices.py,sha256=t29mGFr-_KSLUomgeNDHZcKGbtSIp9gLlckKsLDHAww,17454
|
|
27
27
|
src/tools/events.py,sha256=rnPu8XIGhjkP-KcXk2-J6JRJnMgAsJNkIFWxHww43zs,6070
|
|
@@ -32,22 +32,22 @@ src/tools/port_forwards.py,sha256=H8K0DMtwqUBCO9kixUzb9PuoYhnE_CJE7nwfhOk1v44,25
|
|
|
32
32
|
src/tools/qos.py,sha256=Ym3W5Y7zrRLfH9xqy5Tz2-amYn9QnoIvFG3F5X3FQzI,21554
|
|
33
33
|
src/tools/routing.py,sha256=vaf4UX2MiDmCC9RvzLgLWXTVSEdzUwM6qMpUHaxz67I,11263
|
|
34
34
|
src/tools/stats.py,sha256=1lu18a_0StcfhFr7jTpqbOnuKxvBJfhGSnlY58vWK8Y,8672
|
|
35
|
-
src/tools/system.py,sha256
|
|
35
|
+
src/tools/system.py,sha256=-EvEplS2UiAMHWER6Xy9tRkK_jCK6sDWm3bhR2Nomyc,6309
|
|
36
36
|
src/tools/traffic_routes.py,sha256=cQvqCf5WZOHLuLdvPCLztsjXy-EciAVO3wNG6wlJgPM,9051
|
|
37
37
|
src/tools/usergroups.py,sha256=S5tIj99YHmStuyQPgD-h1-Do9JvbZpM8bFk8XfuON2k,8459
|
|
38
38
|
src/tools/vpn.py,sha256=aBvACvdm-xQcEFFK42JzYlG9OSmwqA2QIvsb6d-UG_s,7500
|
|
39
39
|
src/utils/config_helpers.py,sha256=L9UYSTD1BKsy5KsFag8jVyHbsUA_leUAHRXSMA-ZFZs,1131
|
|
40
40
|
src/utils/confirmation.py,sha256=_GfvEaiyWIC53wmfVHFo8pcjGwmzajnc7EuuqvZ63RI,7045
|
|
41
41
|
src/utils/diagnostics.py,sha256=kDQlVHm5SLUU7t_X4F4zamHSCUujTLrMIploVBTdTTU,5673
|
|
42
|
-
src/utils/lazy_tool_loader.py,sha256=
|
|
42
|
+
src/utils/lazy_tool_loader.py,sha256=FgQ2orQficW_A6473dpP0slQuoA7eiNhHSEEM8TTJE8,7094
|
|
43
43
|
src/utils/meta_tools.py,sha256=XlLkzFEvrZsDEiXxMq_Lz22ZGZ8mMzbjx6krYJS-JD4,13172
|
|
44
|
-
src/utils/permissions.py,sha256=
|
|
44
|
+
src/utils/permissions.py,sha256=dILy7uSb7WJsCfpYKr18VpEts3HfUQhNhNGoRV9IExU,4080
|
|
45
45
|
src/utils/tool_loader.py,sha256=QpfHOxyZ_bzPJM0SItB7UM8uW_p7vl2Sm25rfgfwsAY,4147
|
|
46
|
-
src/tools_manifest.json,sha256=
|
|
47
|
-
src/config/config.yaml,sha256=
|
|
48
|
-
unifi_network_mcp-0.
|
|
49
|
-
unifi_network_mcp-0.
|
|
50
|
-
unifi_network_mcp-0.
|
|
51
|
-
unifi_network_mcp-0.
|
|
52
|
-
unifi_network_mcp-0.
|
|
53
|
-
unifi_network_mcp-0.
|
|
46
|
+
src/tools_manifest.json,sha256=FX4_TrE4gs8Ao_4B40yinipifyE256fzbwhia9W4-Zs,43100
|
|
47
|
+
src/config/config.yaml,sha256=KiYwuDyq5L_6QZL-xG1N1gvO7W3Tck1JYZ-fv1YOi8U,4170
|
|
48
|
+
unifi_network_mcp-0.5.1.data/data/share/unifi-network-mcp/.well-known/mcp-server.json,sha256=Ru2oF_SdOzkathJVTQ6R0bRB-NTmiXLlNVvsp7GqmPs,1427
|
|
49
|
+
unifi_network_mcp-0.5.1.dist-info/METADATA,sha256=cRkmeIzNQMJPwgqped05WGevxLsF2Iz59oCyGu5dhTI,38820
|
|
50
|
+
unifi_network_mcp-0.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
51
|
+
unifi_network_mcp-0.5.1.dist-info/entry_points.txt,sha256=iDs5JXOBoUROpkuTXoxr-1WSrz1FLEgI0TO4pFsZ8u0,52
|
|
52
|
+
unifi_network_mcp-0.5.1.dist-info/licenses/LICENSE,sha256=1tMHOACX4Nse1DzgSKufrvTGztT8kS71LE8l29IAvto,1068
|
|
53
|
+
unifi_network_mcp-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|