unifi-network-mcp 0.4.2__tar.gz → 0.5.0__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.
Files changed (55) hide show
  1. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/PKG-INFO +1 -1
  2. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/_version.py +2 -2
  3. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/config/config.yaml +5 -1
  4. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/main.py +2 -1
  5. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/runtime.py +32 -1
  6. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/clients.py +4 -4
  7. unifi_network_mcp-0.5.0/src/tools/system.py +168 -0
  8. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools_manifest.json +119 -1
  9. unifi_network_mcp-0.5.0/src/utils/lazy_tool_loader.py +214 -0
  10. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/permissions.py +1 -0
  11. unifi_network_mcp-0.4.2/src/tools/system.py +0 -76
  12. unifi_network_mcp-0.4.2/src/utils/lazy_tool_loader.py +0 -300
  13. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/.gitignore +0 -0
  14. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/.well-known/mcp-server.json +0 -0
  15. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/LICENSE +0 -0
  16. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/README.md +0 -0
  17. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/pyproject.toml +0 -0
  18. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/bootstrap.py +0 -0
  19. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/jobs.py +0 -0
  20. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/client_manager.py +0 -0
  21. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/connection_manager.py +0 -0
  22. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/device_manager.py +0 -0
  23. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/event_manager.py +0 -0
  24. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/firewall_manager.py +0 -0
  25. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/hotspot_manager.py +0 -0
  26. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/network_manager.py +0 -0
  27. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/qos_manager.py +0 -0
  28. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/routing_manager.py +0 -0
  29. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/stats_manager.py +0 -0
  30. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/system_manager.py +0 -0
  31. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/traffic_route_manager.py +0 -0
  32. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/usergroup_manager.py +0 -0
  33. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/managers/vpn_manager.py +0 -0
  34. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/schemas.py +0 -0
  35. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tool_index.py +0 -0
  36. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/config.py +0 -0
  37. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/devices.py +0 -0
  38. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/events.py +0 -0
  39. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/firewall.py +0 -0
  40. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/hotspot.py +0 -0
  41. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/network.py +0 -0
  42. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/port_forwards.py +0 -0
  43. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/qos.py +0 -0
  44. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/routing.py +0 -0
  45. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/stats.py +0 -0
  46. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/traffic_routes.py +0 -0
  47. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/usergroups.py +0 -0
  48. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/tools/vpn.py +0 -0
  49. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/config_helpers.py +0 -0
  50. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/confirmation.py +0 -0
  51. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/diagnostics.py +0 -0
  52. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/meta_tools.py +0 -0
  53. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/utils/tool_loader.py +0 -0
  54. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/validator_registry.py +0 -0
  55. {unifi_network_mcp-0.4.2 → unifi_network_mcp-0.5.0}/src/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-mcp
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Unifi Network MCP Server
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
@@ -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.4.2'
32
- __version_tuple__ = version_tuple = (0, 4, 2)
31
+ __version__ = version = '0.5.0'
32
+ __version_tuple__ = version_tuple = (0, 5, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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
@@ -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
- _original_tool_decorator = server.tool # keep reference to wrap later
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
@@ -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
- return FastMCP(
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 ---------------------------------------------------------
@@ -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: Optional[bool] = None,
489
- fixed_ip: Optional[str] = None,
490
- local_dns_record_enabled: Optional[bool] = None,
491
- local_dns_record: Optional[str] = None,
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."""
@@ -0,0 +1,168 @@
1
+ """
2
+ Unifi Network MCP system tools.
3
+
4
+ This module provides MCP tools to interact with a Unifi Network Controller's system functions.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, Optional
9
+
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
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Explicitly retrieve and log the server instance to confirm it's being used
17
+ logger.info(f"System tools module loaded, server instance: {server}")
18
+
19
+
20
+ @server.tool(
21
+ name="unifi_get_system_info",
22
+ description="Get general system information from the Unifi Network controller (version, uptime, etc).",
23
+ )
24
+ async def get_system_info() -> Dict[str, Any]:
25
+ """Implementation for getting system info."""
26
+ logger.info("unifi_get_system_info tool called")
27
+ try:
28
+ info = await system_manager.get_system_info()
29
+ return {
30
+ "success": True,
31
+ "site": system_manager._connection.site,
32
+ "system_info": info,
33
+ }
34
+ except Exception as e:
35
+ logger.error(f"Error getting system info: {e}", exc_info=True)
36
+ return {"success": False, "error": str(e)}
37
+
38
+
39
+ @server.tool(
40
+ name="unifi_get_network_health",
41
+ description="Get the current network health summary (WAN status, device counts).",
42
+ )
43
+ async def get_network_health() -> Dict[str, Any]:
44
+ """Implementation for getting network health."""
45
+ logger.info("unifi_get_network_health tool called")
46
+ try:
47
+ health = await system_manager.get_network_health()
48
+ return {
49
+ "success": True,
50
+ "site": system_manager._connection.site,
51
+ "health_summary": health,
52
+ }
53
+ except Exception as e:
54
+ logger.error(f"Error getting network health: {e}", exc_info=True)
55
+ return {"success": False, "error": str(e)}
56
+
57
+
58
+ @server.tool(
59
+ name="unifi_get_site_settings",
60
+ description="Get current site settings (e.g., country code, timezone, connectivity monitoring).",
61
+ )
62
+ async def get_site_settings() -> Dict[str, Any]:
63
+ """Implementation for getting site settings."""
64
+ logger.info("unifi_get_site_settings tool called")
65
+ try:
66
+ settings = await system_manager.get_site_settings()
67
+ return {
68
+ "success": True,
69
+ "site": system_manager._connection.site,
70
+ "site_settings": settings,
71
+ }
72
+ except Exception as e:
73
+ logger.error(f"Error getting site settings: {e}", exc_info=True)
74
+ return {"success": False, "error": str(e)}
75
+
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
+
164
+ # Print confirmation that all tools have been registered
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
+ )
@@ -1,6 +1,91 @@
1
1
  {
2
- "count": 81,
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",
@@ -0,0 +1,214 @@
1
+ """Lazy tool loader for on-demand tool registration.
2
+
3
+ This module implements true lazy loading of tools, registering them only
4
+ when first called by an LLM. This dramatically reduces initial context usage.
5
+ """
6
+
7
+ import importlib
8
+ import json
9
+ import logging
10
+ import re
11
+ from functools import wraps
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, Set
14
+
15
+ logger = logging.getLogger("unifi-network-mcp")
16
+
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
+
49
+ def _build_tool_module_map() -> Dict[str, str]:
50
+ """Build tool-to-module mapping by scanning tool files.
51
+
52
+ This dynamically discovers all tools and their modules, eliminating the need
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).
57
+ """
58
+ tool_map: Dict[str, str] = {}
59
+
60
+ # Find the tools directory
61
+ # Try relative to this file first, then fall back to cwd
62
+ this_dir = Path(__file__).parent
63
+ tools_dir = this_dir.parent / "tools"
64
+
65
+ if not tools_dir.exists():
66
+ tools_dir = Path("src/tools")
67
+
68
+ if not tools_dir.exists():
69
+ logger.warning("Tools directory not found, falling back to manifest")
70
+ return _load_module_map_from_manifest()
71
+
72
+ # Scan each .py file in tools directory
73
+ for tool_file in tools_dir.glob("*.py"):
74
+ if tool_file.name.startswith("_"):
75
+ continue
76
+
77
+ module_name = f"src.tools.{tool_file.stem}"
78
+
79
+ try:
80
+ # Read file and look for @server.tool or @permissioned_tool decorators
81
+ content = tool_file.read_text()
82
+
83
+ # Find tool names using simple pattern matching
84
+ # Looking for: name="unifi_xxx" or name='unifi_xxx'
85
+ # Note: pattern uses literal 'unifi_' prefix, not a character class
86
+ pattern = r'name\s*=\s*["\'](unifi_[a-z_]+)["\']'
87
+ matches = re.findall(pattern, content)
88
+
89
+ for tool_name in matches:
90
+ if tool_name.startswith("unifi_"):
91
+ tool_map[tool_name] = module_name
92
+
93
+ except Exception as e:
94
+ logger.debug(f"Error scanning {tool_file}: {e}")
95
+
96
+ logger.debug(f"Built dynamic tool map with {len(tool_map)} tools")
97
+ return tool_map
98
+
99
+
100
+ # Build the tool map dynamically at module load time
101
+ # Falls back to manifest if dynamic discovery fails
102
+ TOOL_MODULE_MAP: Dict[str, str] = _build_tool_module_map()
103
+
104
+
105
+ class LazyToolLoader:
106
+ """Manages lazy/on-demand tool loading."""
107
+
108
+ def __init__(self, server, tool_decorator: Callable):
109
+ """Initialize the lazy tool loader.
110
+
111
+ Args:
112
+ server: FastMCP server instance
113
+ tool_decorator: The decorator function to register tools
114
+ """
115
+ self.server = server
116
+ self.tool_decorator = tool_decorator
117
+ self.loaded_modules: Set[str] = set()
118
+ self.loaded_tools: Set[str] = set()
119
+ self._loading = False
120
+
121
+ logger.info("Lazy tool loader initialized")
122
+
123
+ def is_loaded(self, tool_name: str) -> bool:
124
+ """Check if a tool is already loaded."""
125
+ return tool_name in self.loaded_tools
126
+
127
+ async def load_tool(self, tool_name: str) -> bool:
128
+ """Load a tool on-demand.
129
+
130
+ Args:
131
+ tool_name: Name of the tool to load
132
+
133
+ Returns:
134
+ True if tool was loaded successfully, False otherwise
135
+ """
136
+ # Avoid recursive loading
137
+ if self._loading:
138
+ return False
139
+
140
+ if self.is_loaded(tool_name):
141
+ logger.debug(f"Tool '{tool_name}' already loaded")
142
+ return True
143
+
144
+ module_path = TOOL_MODULE_MAP.get(tool_name)
145
+ if not module_path:
146
+ logger.warning(f"No module mapping found for tool '{tool_name}'")
147
+ return False
148
+
149
+ try:
150
+ self._loading = True
151
+ logger.info(f"🔄 Lazy-loading tool '{tool_name}' from '{module_path}'")
152
+
153
+ # Import the module (this will trigger @server.tool decorators)
154
+ if module_path not in self.loaded_modules:
155
+ importlib.import_module(module_path)
156
+ self.loaded_modules.add(module_path)
157
+
158
+ # Mark tool as loaded
159
+ self.loaded_tools.add(tool_name)
160
+
161
+ logger.info(f"✅ Tool '{tool_name}' loaded successfully")
162
+ return True
163
+
164
+ except Exception as e:
165
+ logger.error(f"Failed to load tool '{tool_name}': {e}", exc_info=True)
166
+ return False
167
+ finally:
168
+ self._loading = False
169
+
170
+ async def intercept_call_tool(self, original_call_tool: Callable, name: str, arguments: dict) -> Any:
171
+ """Intercept tool calls to load tools on-demand.
172
+
173
+ Args:
174
+ original_call_tool: Original call_tool method
175
+ name: Tool name
176
+ arguments: Tool arguments
177
+
178
+ Returns:
179
+ Result from the tool execution
180
+ """
181
+ # Try to load the tool if not already loaded
182
+ if not self.is_loaded(name) and name in TOOL_MODULE_MAP:
183
+ loaded = await self.load_tool(name)
184
+ if not loaded:
185
+ raise ValueError(f"Failed to load tool '{name}'")
186
+
187
+ # Call the original method
188
+ return await original_call_tool(name, arguments)
189
+
190
+
191
+ def setup_lazy_loading(server, tool_decorator: Callable) -> LazyToolLoader:
192
+ """Setup lazy tool loading by intercepting call_tool.
193
+
194
+ Args:
195
+ server: FastMCP server instance
196
+ tool_decorator: The decorator function to register tools
197
+
198
+ Returns:
199
+ LazyToolLoader instance
200
+ """
201
+ loader = LazyToolLoader(server, tool_decorator)
202
+
203
+ # Intercept call_tool to load tools on-demand
204
+ original_call_tool = server.call_tool
205
+
206
+ @wraps(original_call_tool)
207
+ async def lazy_call_tool(name: str, arguments: dict):
208
+ return await loader.intercept_call_tool(original_call_tool, name, arguments)
209
+
210
+ server.call_tool = lazy_call_tool
211
+
212
+ logger.info("✨ Lazy tool loading enabled - tools will be loaded on first use")
213
+
214
+ return loader
@@ -35,6 +35,7 @@ CATEGORY_MAP = {
35
35
  "voucher": "vouchers",
36
36
  "usergroup": "usergroups",
37
37
  "route": "routes",
38
+ "snmp": "snmp",
38
39
  }
39
40
 
40
41
  DEFAULT_PERMISSIONS_KEY = "default"
@@ -1,76 +0,0 @@
1
- """
2
- Unifi Network MCP system tools.
3
-
4
- This module provides MCP tools to interact with a Unifi Network Controller's system functions.
5
- """
6
-
7
- import logging
8
- from typing import Any, Dict
9
-
10
- from src.runtime import server, system_manager
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
- # Explicitly retrieve and log the server instance to confirm it's being used
15
- logger.info(f"System tools module loaded, server instance: {server}")
16
-
17
-
18
- @server.tool(
19
- name="unifi_get_system_info",
20
- description="Get general system information from the Unifi Network controller (version, uptime, etc).",
21
- )
22
- async def get_system_info() -> Dict[str, Any]:
23
- """Implementation for getting system info."""
24
- logger.info("unifi_get_system_info tool called")
25
- try:
26
- info = await system_manager.get_system_info()
27
- return {
28
- "success": True,
29
- "site": system_manager._connection.site,
30
- "system_info": info,
31
- }
32
- except Exception as e:
33
- logger.error(f"Error getting system info: {e}", exc_info=True)
34
- return {"success": False, "error": str(e)}
35
-
36
-
37
- @server.tool(
38
- name="unifi_get_network_health",
39
- description="Get the current network health summary (WAN status, device counts).",
40
- )
41
- async def get_network_health() -> Dict[str, Any]:
42
- """Implementation for getting network health."""
43
- logger.info("unifi_get_network_health tool called")
44
- try:
45
- health = await system_manager.get_network_health()
46
- return {
47
- "success": True,
48
- "site": system_manager._connection.site,
49
- "health_summary": health,
50
- }
51
- except Exception as e:
52
- logger.error(f"Error getting network health: {e}", exc_info=True)
53
- return {"success": False, "error": str(e)}
54
-
55
-
56
- @server.tool(
57
- name="unifi_get_site_settings",
58
- description="Get current site settings (e.g., country code, timezone, connectivity monitoring).",
59
- )
60
- async def get_site_settings() -> Dict[str, Any]:
61
- """Implementation for getting site settings."""
62
- logger.info("unifi_get_site_settings tool called")
63
- try:
64
- settings = await system_manager.get_site_settings()
65
- return {
66
- "success": True,
67
- "site": system_manager._connection.site,
68
- "site_settings": settings,
69
- }
70
- except Exception as e:
71
- logger.error(f"Error getting site settings: {e}", exc_info=True)
72
- return {"success": False, "error": str(e)}
73
-
74
-
75
- # Print confirmation that all tools have been registered
76
- logger.info("System tools registered: unifi_get_system_info, unifi_get_network_health, unifi_get_site_settings")
@@ -1,300 +0,0 @@
1
- """Lazy tool loader for on-demand tool registration.
2
-
3
- This module implements true lazy loading of tools, registering them only
4
- when first called by an LLM. This dramatically reduces initial context usage.
5
- """
6
-
7
- import importlib
8
- import logging
9
- from functools import wraps
10
- from pathlib import Path
11
- from typing import Any, Callable, Dict, Set
12
-
13
- logger = logging.getLogger("unifi-network-mcp")
14
-
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] = {
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
- # 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
-
190
-
191
- class LazyToolLoader:
192
- """Manages lazy/on-demand tool loading."""
193
-
194
- def __init__(self, server, tool_decorator: Callable):
195
- """Initialize the lazy tool loader.
196
-
197
- Args:
198
- server: FastMCP server instance
199
- tool_decorator: The decorator function to register tools
200
- """
201
- self.server = server
202
- self.tool_decorator = tool_decorator
203
- self.loaded_modules: Set[str] = set()
204
- self.loaded_tools: Set[str] = set()
205
- self._loading = False
206
-
207
- logger.info("Lazy tool loader initialized")
208
-
209
- def is_loaded(self, tool_name: str) -> bool:
210
- """Check if a tool is already loaded."""
211
- return tool_name in self.loaded_tools
212
-
213
- async def load_tool(self, tool_name: str) -> bool:
214
- """Load a tool on-demand.
215
-
216
- Args:
217
- tool_name: Name of the tool to load
218
-
219
- Returns:
220
- True if tool was loaded successfully, False otherwise
221
- """
222
- # Avoid recursive loading
223
- if self._loading:
224
- return False
225
-
226
- if self.is_loaded(tool_name):
227
- logger.debug(f"Tool '{tool_name}' already loaded")
228
- return True
229
-
230
- module_path = TOOL_MODULE_MAP.get(tool_name)
231
- if not module_path:
232
- logger.warning(f"No module mapping found for tool '{tool_name}'")
233
- return False
234
-
235
- try:
236
- self._loading = True
237
- logger.info(f"🔄 Lazy-loading tool '{tool_name}' from '{module_path}'")
238
-
239
- # Import the module (this will trigger @server.tool decorators)
240
- if module_path not in self.loaded_modules:
241
- importlib.import_module(module_path)
242
- self.loaded_modules.add(module_path)
243
-
244
- # Mark tool as loaded
245
- self.loaded_tools.add(tool_name)
246
-
247
- logger.info(f"✅ Tool '{tool_name}' loaded successfully")
248
- return True
249
-
250
- except Exception as e:
251
- logger.error(f"Failed to load tool '{tool_name}': {e}", exc_info=True)
252
- return False
253
- finally:
254
- self._loading = False
255
-
256
- async def intercept_call_tool(self, original_call_tool: Callable, name: str, arguments: dict) -> Any:
257
- """Intercept tool calls to load tools on-demand.
258
-
259
- Args:
260
- original_call_tool: Original call_tool method
261
- name: Tool name
262
- arguments: Tool arguments
263
-
264
- Returns:
265
- Result from the tool execution
266
- """
267
- # Try to load the tool if not already loaded
268
- if not self.is_loaded(name) and name in TOOL_MODULE_MAP:
269
- loaded = await self.load_tool(name)
270
- if not loaded:
271
- raise ValueError(f"Failed to load tool '{name}'")
272
-
273
- # Call the original method
274
- return await original_call_tool(name, arguments)
275
-
276
-
277
- def setup_lazy_loading(server, tool_decorator: Callable) -> LazyToolLoader:
278
- """Setup lazy tool loading by intercepting call_tool.
279
-
280
- Args:
281
- server: FastMCP server instance
282
- tool_decorator: The decorator function to register tools
283
-
284
- Returns:
285
- LazyToolLoader instance
286
- """
287
- loader = LazyToolLoader(server, tool_decorator)
288
-
289
- # Intercept call_tool to load tools on-demand
290
- original_call_tool = server.call_tool
291
-
292
- @wraps(original_call_tool)
293
- async def lazy_call_tool(name: str, arguments: dict):
294
- return await loader.intercept_call_tool(original_call_tool, name, arguments)
295
-
296
- server.call_tool = lazy_call_tool
297
-
298
- logger.info("✨ Lazy tool loading enabled - tools will be loaded on first use")
299
-
300
- return loader