ui-cli 1.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 (46) hide show
  1. ui_cli/__init__.py +31 -0
  2. ui_cli/client.py +269 -0
  3. ui_cli/commands/__init__.py +1 -0
  4. ui_cli/commands/devices.py +187 -0
  5. ui_cli/commands/groups.py +503 -0
  6. ui_cli/commands/hosts.py +114 -0
  7. ui_cli/commands/isp.py +100 -0
  8. ui_cli/commands/local/__init__.py +63 -0
  9. ui_cli/commands/local/apgroups.py +445 -0
  10. ui_cli/commands/local/clients.py +1537 -0
  11. ui_cli/commands/local/config.py +758 -0
  12. ui_cli/commands/local/devices.py +570 -0
  13. ui_cli/commands/local/dpi.py +369 -0
  14. ui_cli/commands/local/events.py +289 -0
  15. ui_cli/commands/local/firewall.py +285 -0
  16. ui_cli/commands/local/health.py +195 -0
  17. ui_cli/commands/local/networks.py +426 -0
  18. ui_cli/commands/local/portfwd.py +153 -0
  19. ui_cli/commands/local/stats.py +234 -0
  20. ui_cli/commands/local/utils.py +85 -0
  21. ui_cli/commands/local/vouchers.py +410 -0
  22. ui_cli/commands/local/wan.py +302 -0
  23. ui_cli/commands/local/wlans.py +257 -0
  24. ui_cli/commands/mcp.py +416 -0
  25. ui_cli/commands/sdwan.py +168 -0
  26. ui_cli/commands/sites.py +65 -0
  27. ui_cli/commands/speedtest.py +192 -0
  28. ui_cli/commands/status.py +410 -0
  29. ui_cli/commands/version.py +13 -0
  30. ui_cli/config.py +106 -0
  31. ui_cli/groups.py +567 -0
  32. ui_cli/local_client.py +897 -0
  33. ui_cli/main.py +61 -0
  34. ui_cli/models.py +188 -0
  35. ui_cli/output.py +251 -0
  36. ui_cli-1.2.1.dist-info/METADATA +1315 -0
  37. ui_cli-1.2.1.dist-info/RECORD +46 -0
  38. ui_cli-1.2.1.dist-info/WHEEL +4 -0
  39. ui_cli-1.2.1.dist-info/entry_points.txt +3 -0
  40. ui_cli-1.2.1.dist-info/licenses/LICENSE +21 -0
  41. ui_mcp/ARCHITECTURE.md +243 -0
  42. ui_mcp/README.md +235 -0
  43. ui_mcp/__init__.py +7 -0
  44. ui_mcp/__main__.py +10 -0
  45. ui_mcp/cli_runner.py +112 -0
  46. ui_mcp/server.py +468 -0
ui_mcp/server.py ADDED
@@ -0,0 +1,468 @@
1
+ """UI-CLI MCP Server v2.
2
+
3
+ FastMCP server that exposes UniFi management tools via CLI subprocess calls.
4
+ """
5
+
6
+ import json
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ from ui_mcp.cli_runner import run_cli, format_result
11
+
12
+ # Initialize FastMCP server
13
+ server = FastMCP(
14
+ "ui-cli",
15
+ instructions="Manage UniFi network infrastructure - check status, manage clients and devices",
16
+ )
17
+
18
+
19
+ # =============================================================================
20
+ # Status & Health Tools
21
+ # =============================================================================
22
+
23
+
24
+ @server.tool()
25
+ async def network_status() -> str:
26
+ """Check UniFi API connection status.
27
+
28
+ Returns connection status for both Cloud API and Local Controller.
29
+ """
30
+ result = run_cli(["status"])
31
+ if "error" in result:
32
+ return format_result(result)
33
+ return format_result(result, "Network API status retrieved.")
34
+
35
+
36
+ @server.tool()
37
+ async def network_health() -> str:
38
+ """Get network health summary.
39
+
40
+ Returns health status for WAN, LAN, WLAN, and VPN subsystems.
41
+ """
42
+ result = run_cli(["lo", "health"])
43
+ if "error" in result:
44
+ return format_result(result)
45
+
46
+ # Build summary from health data
47
+ if isinstance(result, list):
48
+ subsystems = []
49
+ for h in result:
50
+ name = h.get("subsystem", "unknown")
51
+ status = h.get("status", "unknown")
52
+ subsystems.append(f"{name}: {status}")
53
+ summary = f"Health: {', '.join(subsystems)}"
54
+ else:
55
+ summary = "Health data retrieved."
56
+
57
+ return format_result(result, summary)
58
+
59
+
60
+ @server.tool()
61
+ async def internet_speed() -> str:
62
+ """Get the last speed test result.
63
+
64
+ Returns download/upload speeds and latency from the most recent test.
65
+ """
66
+ result = run_cli(["speedtest"])
67
+ if "error" in result:
68
+ return format_result(result)
69
+ return format_result(result, "Speed test results retrieved.")
70
+
71
+
72
+ @server.tool()
73
+ async def run_speedtest() -> str:
74
+ """Run a new internet speed test.
75
+
76
+ Initiates a speed test on the gateway. Takes 30-60 seconds to complete.
77
+ """
78
+ result = run_cli(["speedtest", "-r"], timeout=90)
79
+ if "error" in result:
80
+ return format_result(result)
81
+ return format_result(result, "Speed test completed.")
82
+
83
+
84
+ @server.tool()
85
+ async def isp_performance(hours: int = 168) -> str:
86
+ """Get ISP performance metrics over time.
87
+
88
+ Args:
89
+ hours: Hours of data to retrieve (default: 168 = 7 days)
90
+
91
+ Returns latency, download/upload speeds, and uptime statistics.
92
+ """
93
+ result = run_cli(["isp", "metrics", "--hours", str(hours)])
94
+ if "error" in result:
95
+ return format_result(result)
96
+ return format_result(result, f"ISP metrics for last {hours} hours.")
97
+
98
+
99
+ # =============================================================================
100
+ # Client Count & Summary Tools
101
+ # =============================================================================
102
+
103
+
104
+ @server.tool()
105
+ async def client_count(by: str = "type") -> str:
106
+ """Count connected clients grouped by category.
107
+
108
+ Args:
109
+ by: Group by 'type' (wired/wireless), 'network', 'vendor', 'ap', or 'experience'
110
+
111
+ Returns client counts without listing individual clients.
112
+ """
113
+ result = run_cli(["lo", "clients", "count", "--by", by])
114
+ if "error" in result:
115
+ return format_result(result)
116
+
117
+ # Build summary
118
+ counts = result.get("counts", {})
119
+ total = result.get("total", sum(counts.values()))
120
+ summary = f"Total: {total} clients. " + ", ".join(
121
+ f"{k}: {v}" for k, v in counts.items()
122
+ )
123
+ return format_result(result, summary)
124
+
125
+
126
+ @server.tool()
127
+ async def device_list() -> str:
128
+ """List UniFi network devices (APs, switches, gateways).
129
+
130
+ Returns all managed UniFi devices with status and firmware info.
131
+ """
132
+ result = run_cli(["lo", "devices", "list"])
133
+ if "error" in result:
134
+ return format_result(result)
135
+
136
+ # Build summary
137
+ if isinstance(result, list):
138
+ count = len(result)
139
+ types = {}
140
+ for d in result:
141
+ t = d.get("type", "unknown")
142
+ types[t] = types.get(t, 0) + 1
143
+ type_str = ", ".join(f"{v} {k}" for k, v in types.items())
144
+ summary = f"Found {count} devices: {type_str}"
145
+ else:
146
+ summary = "Device list retrieved."
147
+
148
+ return format_result(result, summary)
149
+
150
+
151
+ @server.tool()
152
+ async def network_list() -> str:
153
+ """List configured networks and VLANs.
154
+
155
+ Returns all networks with VLAN IDs, subnets, and DHCP settings.
156
+ """
157
+ result = run_cli(["lo", "networks", "list"])
158
+ if "error" in result:
159
+ return format_result(result)
160
+
161
+ # Build summary
162
+ if isinstance(result, list):
163
+ names = [n.get("name", "unnamed") for n in result]
164
+ summary = f"Found {len(result)} networks: {', '.join(names)}"
165
+ else:
166
+ summary = "Network list retrieved."
167
+
168
+ return format_result(result, summary)
169
+
170
+
171
+ # =============================================================================
172
+ # Lookup Tools
173
+ # =============================================================================
174
+
175
+
176
+ @server.tool()
177
+ async def find_client(name: str) -> str:
178
+ """Find a specific client by name or MAC address.
179
+
180
+ Args:
181
+ name: Client name, hostname, or MAC address
182
+
183
+ Returns detailed client info including IP, connection type, and status.
184
+ """
185
+ result = run_cli(["lo", "clients", "get", name])
186
+ if "error" in result:
187
+ return format_result(result)
188
+ return format_result(result, f"Found client: {name}")
189
+
190
+
191
+ @server.tool()
192
+ async def find_device(name: str) -> str:
193
+ """Find a specific UniFi device by name, MAC, or IP.
194
+
195
+ Args:
196
+ name: Device name, MAC address, or IP address
197
+
198
+ Returns detailed device info including model, firmware, and status.
199
+ """
200
+ result = run_cli(["lo", "devices", "get", name])
201
+ if "error" in result:
202
+ return format_result(result)
203
+ return format_result(result, f"Found device: {name}")
204
+
205
+
206
+ @server.tool()
207
+ async def client_status(name: str) -> str:
208
+ """Check if a client is online and/or blocked.
209
+
210
+ Args:
211
+ name: Client name, hostname, or MAC address
212
+
213
+ Returns connection status, block status, and network info.
214
+ """
215
+ result = run_cli(["lo", "clients", "status", name])
216
+ if "error" in result:
217
+ return format_result(result)
218
+
219
+ # Build summary
220
+ online = result.get("online", False)
221
+ blocked = result.get("blocked", False)
222
+ status_parts = []
223
+ status_parts.append("online" if online else "offline")
224
+ if blocked:
225
+ status_parts.append("BLOCKED")
226
+ summary = f"{name}: {', '.join(status_parts)}"
227
+
228
+ return format_result(result, summary)
229
+
230
+
231
+ # =============================================================================
232
+ # Action Tools
233
+ # =============================================================================
234
+
235
+
236
+ @server.tool()
237
+ async def block_client(name: str) -> str:
238
+ """Block a client from the network.
239
+
240
+ Args:
241
+ name: Client name, hostname, or MAC address to block
242
+
243
+ The client will be disconnected and prevented from reconnecting.
244
+ """
245
+ result = run_cli(["lo", "clients", "block", name])
246
+ if "error" in result:
247
+ return format_result(result)
248
+
249
+ success = result.get("success", False)
250
+ if success:
251
+ return format_result(result, f"Blocked client: {name}")
252
+ else:
253
+ return format_result(result, f"Failed to block: {name}")
254
+
255
+
256
+ @server.tool()
257
+ async def unblock_client(name: str) -> str:
258
+ """Unblock a previously blocked client.
259
+
260
+ Args:
261
+ name: Client name, hostname, or MAC address to unblock
262
+
263
+ The client will be able to reconnect to the network.
264
+ """
265
+ result = run_cli(["lo", "clients", "unblock", name])
266
+ if "error" in result:
267
+ return format_result(result)
268
+
269
+ success = result.get("success", False)
270
+ if success:
271
+ return format_result(result, f"Unblocked client: {name}")
272
+ else:
273
+ return format_result(result, f"Failed to unblock: {name}")
274
+
275
+
276
+ @server.tool()
277
+ async def kick_client(name: str) -> str:
278
+ """Disconnect a client from the network.
279
+
280
+ Args:
281
+ name: Client name, hostname, or MAC address to disconnect
282
+
283
+ The client will be disconnected but can reconnect immediately.
284
+ """
285
+ result = run_cli(["lo", "clients", "kick", name])
286
+ if "error" in result:
287
+ return format_result(result)
288
+
289
+ success = result.get("success", False)
290
+ if success:
291
+ return format_result(result, f"Kicked client: {name}")
292
+ else:
293
+ return format_result(result, f"Failed to kick: {name}")
294
+
295
+
296
+ @server.tool()
297
+ async def restart_device(name: str) -> str:
298
+ """Restart a UniFi device.
299
+
300
+ Args:
301
+ name: Device name, MAC address, or IP address
302
+
303
+ The device will reboot and be offline for 1-3 minutes.
304
+ """
305
+ result = run_cli(["lo", "devices", "restart", name])
306
+ if "error" in result:
307
+ return format_result(result)
308
+
309
+ success = result.get("success", False)
310
+ if success:
311
+ return format_result(result, f"Restarting device: {name}")
312
+ else:
313
+ return format_result(result, f"Failed to restart: {name}")
314
+
315
+
316
+ @server.tool()
317
+ async def create_voucher(
318
+ duration_hours: int = 24,
319
+ count: int = 1,
320
+ ) -> str:
321
+ """Create guest WiFi voucher(s).
322
+
323
+ Args:
324
+ duration_hours: How long the voucher is valid (default: 24 hours)
325
+ count: Number of vouchers to create (default: 1)
326
+
327
+ Returns the voucher code(s) that guests can use to access WiFi.
328
+ """
329
+ duration_minutes = duration_hours * 60
330
+ result = run_cli([
331
+ "lo", "vouchers", "create",
332
+ "--count", str(count),
333
+ "--duration", str(duration_minutes),
334
+ ])
335
+ if "error" in result:
336
+ return format_result(result)
337
+
338
+ vouchers = result.get("vouchers", [])
339
+ if vouchers:
340
+ codes = [v.get("code", "") for v in vouchers]
341
+ summary = f"Created {len(vouchers)} voucher(s): {', '.join(codes)}"
342
+ else:
343
+ summary = "Voucher creation response received."
344
+
345
+ return format_result(result, summary)
346
+
347
+
348
+ # =============================================================================
349
+ # Client Groups Tools
350
+ # =============================================================================
351
+
352
+
353
+ @server.tool()
354
+ async def list_groups() -> str:
355
+ """List all client groups.
356
+
357
+ Returns all defined groups with member counts.
358
+ Groups can be used for bulk actions like blocking/unblocking.
359
+ """
360
+ result = run_cli(["groups", "list"])
361
+ if "error" in result:
362
+ return format_result(result)
363
+
364
+ if isinstance(result, list):
365
+ count = len(result)
366
+ summary = f"Found {count} group(s)"
367
+ else:
368
+ summary = "Groups retrieved."
369
+
370
+ return format_result(result, summary)
371
+
372
+
373
+ @server.tool()
374
+ async def get_group(name: str) -> str:
375
+ """Get details of a client group.
376
+
377
+ Args:
378
+ name: Group name or slug
379
+
380
+ Returns group info including members and rules (for auto groups).
381
+ """
382
+ result = run_cli(["groups", "show", name])
383
+ if "error" in result:
384
+ return format_result(result)
385
+ return format_result(result, f"Group details: {name}")
386
+
387
+
388
+ @server.tool()
389
+ async def block_group(name: str) -> str:
390
+ """Block all clients in a group.
391
+
392
+ Args:
393
+ name: Group name or slug
394
+
395
+ All clients in the group will be blocked from the network.
396
+ Useful for parental controls (e.g., bedtime restrictions).
397
+ """
398
+ result = run_cli(["lo", "clients", "block", "-g", name, "-y"])
399
+ if "error" in result:
400
+ return format_result(result)
401
+
402
+ summary = result.get("summary", {})
403
+ blocked = summary.get("blocked", 0)
404
+ already = summary.get("already", 0)
405
+ failed = summary.get("failed", 0)
406
+ return format_result(
407
+ result,
408
+ f"Blocked {blocked} clients (already blocked: {already}, failed: {failed})"
409
+ )
410
+
411
+
412
+ @server.tool()
413
+ async def unblock_group(name: str) -> str:
414
+ """Unblock all clients in a group.
415
+
416
+ Args:
417
+ name: Group name or slug
418
+
419
+ All previously blocked clients in the group will be unblocked.
420
+ """
421
+ result = run_cli(["lo", "clients", "unblock", "-g", name, "-y"])
422
+ if "error" in result:
423
+ return format_result(result)
424
+
425
+ summary = result.get("summary", {})
426
+ unblocked = summary.get("unblocked", 0)
427
+ not_blocked = summary.get("not_blocked", 0)
428
+ failed = summary.get("failed", 0)
429
+ return format_result(
430
+ result,
431
+ f"Unblocked {unblocked} clients (not blocked: {not_blocked}, failed: {failed})"
432
+ )
433
+
434
+
435
+ @server.tool()
436
+ async def group_status(name: str) -> str:
437
+ """Get live status of all clients in a group.
438
+
439
+ Args:
440
+ name: Group name or slug
441
+
442
+ Returns online/offline status for each group member.
443
+ """
444
+ result = run_cli(["lo", "clients", "list", "-g", name])
445
+ if "error" in result:
446
+ return format_result(result)
447
+
448
+ if isinstance(result, list):
449
+ count = len(result)
450
+ summary = f"Found {count} client(s) in group '{name}'"
451
+ else:
452
+ summary = f"Status for group: {name}"
453
+
454
+ return format_result(result, summary)
455
+
456
+
457
+ # =============================================================================
458
+ # Entry Point
459
+ # =============================================================================
460
+
461
+
462
+ def main():
463
+ """Run the MCP server."""
464
+ server.run()
465
+
466
+
467
+ if __name__ == "__main__":
468
+ main()