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.
- ui_cli/__init__.py +31 -0
- ui_cli/client.py +269 -0
- ui_cli/commands/__init__.py +1 -0
- ui_cli/commands/devices.py +187 -0
- ui_cli/commands/groups.py +503 -0
- ui_cli/commands/hosts.py +114 -0
- ui_cli/commands/isp.py +100 -0
- ui_cli/commands/local/__init__.py +63 -0
- ui_cli/commands/local/apgroups.py +445 -0
- ui_cli/commands/local/clients.py +1537 -0
- ui_cli/commands/local/config.py +758 -0
- ui_cli/commands/local/devices.py +570 -0
- ui_cli/commands/local/dpi.py +369 -0
- ui_cli/commands/local/events.py +289 -0
- ui_cli/commands/local/firewall.py +285 -0
- ui_cli/commands/local/health.py +195 -0
- ui_cli/commands/local/networks.py +426 -0
- ui_cli/commands/local/portfwd.py +153 -0
- ui_cli/commands/local/stats.py +234 -0
- ui_cli/commands/local/utils.py +85 -0
- ui_cli/commands/local/vouchers.py +410 -0
- ui_cli/commands/local/wan.py +302 -0
- ui_cli/commands/local/wlans.py +257 -0
- ui_cli/commands/mcp.py +416 -0
- ui_cli/commands/sdwan.py +168 -0
- ui_cli/commands/sites.py +65 -0
- ui_cli/commands/speedtest.py +192 -0
- ui_cli/commands/status.py +410 -0
- ui_cli/commands/version.py +13 -0
- ui_cli/config.py +106 -0
- ui_cli/groups.py +567 -0
- ui_cli/local_client.py +897 -0
- ui_cli/main.py +61 -0
- ui_cli/models.py +188 -0
- ui_cli/output.py +251 -0
- ui_cli-1.2.1.dist-info/METADATA +1315 -0
- ui_cli-1.2.1.dist-info/RECORD +46 -0
- ui_cli-1.2.1.dist-info/WHEEL +4 -0
- ui_cli-1.2.1.dist-info/entry_points.txt +3 -0
- ui_cli-1.2.1.dist-info/licenses/LICENSE +21 -0
- ui_mcp/ARCHITECTURE.md +243 -0
- ui_mcp/README.md +235 -0
- ui_mcp/__init__.py +7 -0
- ui_mcp/__main__.py +10 -0
- ui_mcp/cli_runner.py +112 -0
- 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()
|