iflow-mcp_enuno-unifi-mcp-server 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
  2. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
  3. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
  4. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
  6. src/__init__.py +3 -0
  7. src/__main__.py +6 -0
  8. src/api/__init__.py +5 -0
  9. src/api/client.py +727 -0
  10. src/api/site_manager_client.py +176 -0
  11. src/cache.py +483 -0
  12. src/config/__init__.py +5 -0
  13. src/config/config.py +321 -0
  14. src/main.py +2234 -0
  15. src/models/__init__.py +126 -0
  16. src/models/acl.py +41 -0
  17. src/models/backup.py +272 -0
  18. src/models/client.py +74 -0
  19. src/models/device.py +53 -0
  20. src/models/dpi.py +50 -0
  21. src/models/firewall_policy.py +123 -0
  22. src/models/firewall_zone.py +28 -0
  23. src/models/network.py +62 -0
  24. src/models/qos_profile.py +458 -0
  25. src/models/radius.py +141 -0
  26. src/models/reference_data.py +34 -0
  27. src/models/site.py +59 -0
  28. src/models/site_manager.py +120 -0
  29. src/models/topology.py +138 -0
  30. src/models/traffic_flow.py +137 -0
  31. src/models/traffic_matching_list.py +56 -0
  32. src/models/voucher.py +42 -0
  33. src/models/vpn.py +73 -0
  34. src/models/wan.py +48 -0
  35. src/models/zbf_matrix.py +49 -0
  36. src/resources/__init__.py +8 -0
  37. src/resources/clients.py +111 -0
  38. src/resources/devices.py +102 -0
  39. src/resources/networks.py +93 -0
  40. src/resources/site_manager.py +64 -0
  41. src/resources/sites.py +86 -0
  42. src/tools/__init__.py +25 -0
  43. src/tools/acls.py +328 -0
  44. src/tools/application.py +42 -0
  45. src/tools/backups.py +1173 -0
  46. src/tools/client_management.py +505 -0
  47. src/tools/clients.py +203 -0
  48. src/tools/device_control.py +325 -0
  49. src/tools/devices.py +354 -0
  50. src/tools/dpi.py +241 -0
  51. src/tools/dpi_tools.py +89 -0
  52. src/tools/firewall.py +417 -0
  53. src/tools/firewall_policies.py +430 -0
  54. src/tools/firewall_zones.py +515 -0
  55. src/tools/network_config.py +388 -0
  56. src/tools/networks.py +190 -0
  57. src/tools/port_forwarding.py +263 -0
  58. src/tools/qos.py +1070 -0
  59. src/tools/radius.py +763 -0
  60. src/tools/reference_data.py +107 -0
  61. src/tools/site_manager.py +466 -0
  62. src/tools/site_vpn.py +95 -0
  63. src/tools/sites.py +187 -0
  64. src/tools/topology.py +406 -0
  65. src/tools/traffic_flows.py +1062 -0
  66. src/tools/traffic_matching_lists.py +371 -0
  67. src/tools/vouchers.py +249 -0
  68. src/tools/vpn.py +76 -0
  69. src/tools/wans.py +30 -0
  70. src/tools/wifi.py +498 -0
  71. src/tools/zbf_matrix.py +326 -0
  72. src/utils/__init__.py +88 -0
  73. src/utils/audit.py +213 -0
  74. src/utils/exceptions.py +114 -0
  75. src/utils/helpers.py +159 -0
  76. src/utils/logger.py +105 -0
  77. src/utils/sanitize.py +244 -0
  78. src/utils/validators.py +160 -0
  79. src/webhooks/__init__.py +6 -0
  80. src/webhooks/handlers.py +196 -0
  81. src/webhooks/receiver.py +290 -0
src/resources/sites.py ADDED
@@ -0,0 +1,86 @@
1
+ """Sites MCP resource implementation."""
2
+
3
+ from ..api import UniFiClient
4
+ from ..config import Settings
5
+ from ..models import Site
6
+ from ..utils import get_logger, validate_limit_offset
7
+
8
+
9
+ class SitesResource:
10
+ """MCP resource for UniFi sites."""
11
+
12
+ def __init__(self, settings: Settings) -> None:
13
+ """Initialize sites resource.
14
+
15
+ Args:
16
+ settings: Application settings
17
+ """
18
+ self.settings = settings
19
+ self.logger = get_logger(__name__, settings.log_level)
20
+
21
+ async def list_sites(self, limit: int | None = None, offset: int | None = None) -> list[Site]:
22
+ """List all UniFi sites.
23
+
24
+ Args:
25
+ limit: Maximum number of sites to return
26
+ offset: Number of sites to skip
27
+
28
+ Returns:
29
+ List of Site objects
30
+ """
31
+ limit, offset = validate_limit_offset(limit, offset)
32
+
33
+ async with UniFiClient(self.settings) as client:
34
+ # Authenticate first
35
+ await client.authenticate()
36
+
37
+ # Fetch sites from API
38
+ response = await client.get("/ea/sites")
39
+
40
+ # Extract sites data
41
+ sites_data = response.get("data", [])
42
+
43
+ # Apply pagination
44
+ paginated_data = sites_data[offset : offset + limit]
45
+
46
+ # Parse into Site models
47
+ sites = [Site(**site) for site in paginated_data]
48
+
49
+ self.logger.info(f"Retrieved {len(sites)} sites (offset={offset}, limit={limit})")
50
+
51
+ return sites
52
+
53
+ async def get_site(self, site_id: str) -> Site | None:
54
+ """Get a specific site by ID.
55
+
56
+ Args:
57
+ site_id: Site identifier
58
+
59
+ Returns:
60
+ Site object or None if not found
61
+ """
62
+ async with UniFiClient(self.settings) as client:
63
+ await client.authenticate()
64
+
65
+ response = await client.get("/ea/sites")
66
+ sites_data = response.get("data", [])
67
+
68
+ # Find the specific site
69
+ for site_data in sites_data:
70
+ if site_data.get("_id") == site_id or site_data.get("name") == site_id:
71
+ return Site(**site_data)
72
+
73
+ return None
74
+
75
+ def get_uri(self, site_id: str | None = None) -> str:
76
+ """Get the MCP resource URI.
77
+
78
+ Args:
79
+ site_id: Optional site ID
80
+
81
+ Returns:
82
+ Resource URI
83
+ """
84
+ if site_id:
85
+ return f"sites://{site_id}"
86
+ return "sites://"
src/tools/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """MCP tools for UniFi MCP Server."""
2
+
3
+ from . import (
4
+ client_management,
5
+ clients,
6
+ device_control,
7
+ devices,
8
+ firewall,
9
+ network_config,
10
+ networks,
11
+ sites,
12
+ )
13
+
14
+ __all__ = [
15
+ # Phase 3: Read Operations
16
+ "devices",
17
+ "clients",
18
+ "networks",
19
+ "sites",
20
+ # Phase 4: Write Operations
21
+ "firewall",
22
+ "network_config",
23
+ "device_control",
24
+ "client_management",
25
+ ]
src/tools/acls.py ADDED
@@ -0,0 +1,328 @@
1
+ """Access Control List (ACL) management tools."""
2
+
3
+ from typing import Any
4
+
5
+ from ..api.client import UniFiClient
6
+ from ..config import Settings
7
+ from ..models import ACLRule
8
+ from ..utils import audit_action, get_logger, validate_confirmation
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ async def list_acl_rules(
14
+ site_id: str,
15
+ settings: Settings,
16
+ limit: int | None = None,
17
+ offset: int | None = None,
18
+ filter_expr: str | None = None,
19
+ ) -> list[dict]:
20
+ """List all ACL rules for a site.
21
+
22
+ Args:
23
+ site_id: Site identifier
24
+ settings: Application settings
25
+ limit: Maximum number of results
26
+ offset: Starting position
27
+ filter_expr: Filter expression
28
+
29
+ Returns:
30
+ List of ACL rules
31
+ """
32
+ async with UniFiClient(settings) as client:
33
+ logger.info(f"Listing ACL rules for site {site_id}")
34
+
35
+ if not client.is_authenticated:
36
+ await client.authenticate()
37
+
38
+ params: dict[str, Any] = {}
39
+ if limit is not None:
40
+ params["limit"] = limit
41
+ if offset is not None:
42
+ params["offset"] = offset
43
+ if filter_expr:
44
+ params["filter"] = filter_expr
45
+
46
+ response = await client.get(f"/integration/v1/sites/{site_id}/acls", params=params)
47
+ data = response.get("data", [])
48
+
49
+ return [ACLRule(**rule).model_dump() for rule in data]
50
+
51
+
52
+ async def get_acl_rule(site_id: str, acl_rule_id: str, settings: Settings) -> dict:
53
+ """Get details for a specific ACL rule.
54
+
55
+ Args:
56
+ site_id: Site identifier
57
+ acl_rule_id: ACL rule identifier
58
+ settings: Application settings
59
+
60
+ Returns:
61
+ ACL rule details
62
+ """
63
+ async with UniFiClient(settings) as client:
64
+ logger.info(f"Getting ACL rule {acl_rule_id} for site {site_id}")
65
+
66
+ if not client.is_authenticated:
67
+ await client.authenticate()
68
+
69
+ response = await client.get(f"/integration/v1/sites/{site_id}/acls/{acl_rule_id}")
70
+ data = response.get("data", response)
71
+
72
+ return ACLRule(**data).model_dump() # type: ignore[no-any-return]
73
+
74
+
75
+ async def create_acl_rule(
76
+ site_id: str,
77
+ name: str,
78
+ action: str,
79
+ settings: Settings,
80
+ enabled: bool = True,
81
+ source_type: str | None = None,
82
+ source_id: str | None = None,
83
+ source_network: str | None = None,
84
+ destination_type: str | None = None,
85
+ destination_id: str | None = None,
86
+ destination_network: str | None = None,
87
+ protocol: str | None = None,
88
+ src_port: int | None = None,
89
+ dst_port: int | None = None,
90
+ priority: int = 100,
91
+ description: str | None = None,
92
+ confirm: bool = False,
93
+ dry_run: bool = False,
94
+ ) -> dict:
95
+ """Create a new ACL rule.
96
+
97
+ Args:
98
+ site_id: Site identifier
99
+ name: Rule name
100
+ action: Action to take (allow/deny)
101
+ settings: Application settings
102
+ enabled: Whether the rule is enabled
103
+ source_type: Source type (network/device/ip/any)
104
+ source_id: Source identifier
105
+ source_network: Source network CIDR
106
+ destination_type: Destination type
107
+ destination_id: Destination identifier
108
+ destination_network: Destination network CIDR
109
+ protocol: Protocol (tcp/udp/icmp/all)
110
+ src_port: Source port
111
+ dst_port: Destination port
112
+ priority: Rule priority (lower = higher priority)
113
+ description: Rule description
114
+ confirm: Confirmation flag (required)
115
+ dry_run: If True, validate but don't execute
116
+
117
+ Returns:
118
+ Created ACL rule
119
+ """
120
+ validate_confirmation(confirm, "create ACL rule")
121
+
122
+ async with UniFiClient(settings) as client:
123
+ logger.info(f"Creating ACL rule '{name}' for site {site_id}")
124
+
125
+ if not client.is_authenticated:
126
+ await client.authenticate()
127
+
128
+ # Build request payload
129
+ payload = {
130
+ "name": name,
131
+ "enabled": enabled,
132
+ "action": action,
133
+ "priority": priority,
134
+ }
135
+
136
+ if description:
137
+ payload["description"] = description
138
+ if source_type:
139
+ payload["sourceType"] = source_type
140
+ if source_id:
141
+ payload["sourceId"] = source_id
142
+ if source_network:
143
+ payload["sourceNetwork"] = source_network
144
+ if destination_type:
145
+ payload["destinationType"] = destination_type
146
+ if destination_id:
147
+ payload["destinationId"] = destination_id
148
+ if destination_network:
149
+ payload["destinationNetwork"] = destination_network
150
+ if protocol:
151
+ payload["protocol"] = protocol
152
+ if src_port is not None:
153
+ payload["srcPort"] = src_port
154
+ if dst_port is not None:
155
+ payload["dstPort"] = dst_port
156
+
157
+ if dry_run:
158
+ logger.info(f"[DRY RUN] Would create ACL rule with payload: {payload}")
159
+ return {"dry_run": True, "payload": payload}
160
+
161
+ response = await client.post(f"/integration/v1/sites/{site_id}/acls", json_data=payload)
162
+ data = response.get("data", response)
163
+
164
+ # Audit the action
165
+ await audit_action(
166
+ settings,
167
+ action_type="create_acl_rule",
168
+ resource_type="acl_rule",
169
+ resource_id=data.get("_id", "unknown"),
170
+ site_id=site_id,
171
+ details={"name": name, "action": action},
172
+ )
173
+
174
+ return ACLRule(**data).model_dump() # type: ignore[no-any-return]
175
+
176
+
177
+ async def update_acl_rule(
178
+ site_id: str,
179
+ acl_rule_id: str,
180
+ settings: Settings,
181
+ name: str | None = None,
182
+ action: str | None = None,
183
+ enabled: bool | None = None,
184
+ source_type: str | None = None,
185
+ source_id: str | None = None,
186
+ source_network: str | None = None,
187
+ destination_type: str | None = None,
188
+ destination_id: str | None = None,
189
+ destination_network: str | None = None,
190
+ protocol: str | None = None,
191
+ src_port: int | None = None,
192
+ dst_port: int | None = None,
193
+ priority: int | None = None,
194
+ description: str | None = None,
195
+ confirm: bool = False,
196
+ dry_run: bool = False,
197
+ ) -> dict:
198
+ """Update an existing ACL rule.
199
+
200
+ Args:
201
+ site_id: Site identifier
202
+ acl_rule_id: ACL rule identifier
203
+ settings: Application settings
204
+ name: Rule name
205
+ action: Action to take
206
+ enabled: Whether the rule is enabled
207
+ source_type: Source type
208
+ source_id: Source identifier
209
+ source_network: Source network CIDR
210
+ destination_type: Destination type
211
+ destination_id: Destination identifier
212
+ destination_network: Destination network CIDR
213
+ protocol: Protocol
214
+ src_port: Source port
215
+ dst_port: Destination port
216
+ priority: Rule priority
217
+ description: Rule description
218
+ confirm: Confirmation flag (required)
219
+ dry_run: If True, validate but don't execute
220
+
221
+ Returns:
222
+ Updated ACL rule
223
+ """
224
+ validate_confirmation(confirm, "update ACL rule")
225
+
226
+ async with UniFiClient(settings) as client:
227
+ logger.info(f"Updating ACL rule {acl_rule_id} for site {site_id}")
228
+
229
+ if not client.is_authenticated:
230
+ await client.authenticate()
231
+
232
+ # Build request payload with only provided fields
233
+ payload: dict[str, Any] = {}
234
+ if name is not None:
235
+ payload["name"] = name
236
+ if action is not None:
237
+ payload["action"] = action
238
+ if enabled is not None:
239
+ payload["enabled"] = enabled
240
+ if priority is not None:
241
+ payload["priority"] = priority
242
+ if description is not None:
243
+ payload["description"] = description
244
+ if source_type is not None:
245
+ payload["sourceType"] = source_type
246
+ if source_id is not None:
247
+ payload["sourceId"] = source_id
248
+ if source_network is not None:
249
+ payload["sourceNetwork"] = source_network
250
+ if destination_type is not None:
251
+ payload["destinationType"] = destination_type
252
+ if destination_id is not None:
253
+ payload["destinationId"] = destination_id
254
+ if destination_network is not None:
255
+ payload["destinationNetwork"] = destination_network
256
+ if protocol is not None:
257
+ payload["protocol"] = protocol
258
+ if src_port is not None:
259
+ payload["srcPort"] = src_port
260
+ if dst_port is not None:
261
+ payload["dstPort"] = dst_port
262
+
263
+ if dry_run:
264
+ logger.info(f"[DRY RUN] Would update ACL rule with payload: {payload}")
265
+ return {"dry_run": True, "payload": payload}
266
+
267
+ response = await client.put(
268
+ f"/integration/v1/sites/{site_id}/acls/{acl_rule_id}", json_data=payload
269
+ )
270
+ data = response.get("data", response)
271
+
272
+ # Audit the action
273
+ await audit_action(
274
+ settings,
275
+ action_type="update_acl_rule",
276
+ resource_type="acl_rule",
277
+ resource_id=acl_rule_id,
278
+ site_id=site_id,
279
+ details=payload,
280
+ )
281
+
282
+ return ACLRule(**data).model_dump() # type: ignore[no-any-return]
283
+
284
+
285
+ async def delete_acl_rule(
286
+ site_id: str,
287
+ acl_rule_id: str,
288
+ settings: Settings,
289
+ confirm: bool = False,
290
+ dry_run: bool = False,
291
+ ) -> dict:
292
+ """Delete an ACL rule.
293
+
294
+ Args:
295
+ site_id: Site identifier
296
+ acl_rule_id: ACL rule identifier
297
+ settings: Application settings
298
+ confirm: Confirmation flag (required)
299
+ dry_run: If True, validate but don't execute
300
+
301
+ Returns:
302
+ Deletion status
303
+ """
304
+ validate_confirmation(confirm, "delete ACL rule")
305
+
306
+ async with UniFiClient(settings) as client:
307
+ logger.info(f"Deleting ACL rule {acl_rule_id} for site {site_id}")
308
+
309
+ if not client.is_authenticated:
310
+ await client.authenticate()
311
+
312
+ if dry_run:
313
+ logger.info(f"[DRY RUN] Would delete ACL rule {acl_rule_id}")
314
+ return {"dry_run": True, "acl_rule_id": acl_rule_id}
315
+
316
+ await client.delete(f"/integration/v1/sites/{site_id}/acls/{acl_rule_id}")
317
+
318
+ # Audit the action
319
+ await audit_action(
320
+ settings,
321
+ action_type="delete_acl_rule",
322
+ resource_type="acl_rule",
323
+ resource_id=acl_rule_id,
324
+ site_id=site_id,
325
+ details={},
326
+ )
327
+
328
+ return {"success": True, "message": f"ACL rule {acl_rule_id} deleted successfully"}
@@ -0,0 +1,42 @@
1
+ """Application information tools."""
2
+
3
+ from ..api.client import UniFiClient
4
+ from ..config import Settings
5
+ from ..utils import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ async def get_application_info(settings: Settings) -> dict:
11
+ """Get UniFi Network application information.
12
+
13
+ Args:
14
+ settings: Application settings
15
+
16
+ Returns:
17
+ Application information dictionary
18
+
19
+ Example:
20
+ >>> info = await get_application_info(settings)
21
+ >>> print(info["version"])
22
+ """
23
+ async with UniFiClient(settings) as client:
24
+ logger.info("Fetching application information")
25
+
26
+ # Authenticate if not already done
27
+ if not client.is_authenticated:
28
+ await client.authenticate()
29
+
30
+ # Get application info
31
+ response = await client.get("/integration/v1/application/info")
32
+
33
+ # Extract data from response
34
+ data = response.get("data", response)
35
+
36
+ return {
37
+ "version": data.get("version"),
38
+ "build": data.get("build"),
39
+ "deployment_type": data.get("deploymentType"),
40
+ "capabilities": data.get("capabilities", []),
41
+ "system_info": data.get("systemInfo", {}),
42
+ }