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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""WLAN management commands for local controller."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ui_cli.commands.local.utils import run_with_spinner
|
|
8
|
+
from ui_cli.local_client import LocalAPIError, UniFiLocalClient
|
|
9
|
+
from ui_cli.output import (
|
|
10
|
+
OutputFormat,
|
|
11
|
+
console,
|
|
12
|
+
output_csv,
|
|
13
|
+
output_json,
|
|
14
|
+
print_error,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(name="wlans", help="WLAN management", no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_security_type(wlan: dict[str, Any]) -> str:
|
|
21
|
+
"""Get human-readable security type for a WLAN."""
|
|
22
|
+
security = wlan.get("security", "open")
|
|
23
|
+
wpa_mode = wlan.get("wpa_mode", "")
|
|
24
|
+
|
|
25
|
+
if security == "open":
|
|
26
|
+
return "Open"
|
|
27
|
+
elif security == "wpapsk":
|
|
28
|
+
if wpa_mode == "wpa2":
|
|
29
|
+
return "WPA2-Personal"
|
|
30
|
+
elif wpa_mode == "wpa3":
|
|
31
|
+
return "WPA3-Personal"
|
|
32
|
+
return "WPA-Personal"
|
|
33
|
+
elif security == "wpaeap":
|
|
34
|
+
if wpa_mode == "wpa2":
|
|
35
|
+
return "WPA2-Enterprise"
|
|
36
|
+
elif wpa_mode == "wpa3":
|
|
37
|
+
return "WPA3-Enterprise"
|
|
38
|
+
return "WPA-Enterprise"
|
|
39
|
+
return security
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_wlan(wlans: list[dict[str, Any]], identifier: str) -> dict[str, Any] | None:
|
|
43
|
+
"""Find WLAN by ID or name."""
|
|
44
|
+
identifier_lower = identifier.lower()
|
|
45
|
+
|
|
46
|
+
# First try exact ID match
|
|
47
|
+
for w in wlans:
|
|
48
|
+
if w.get("_id") == identifier:
|
|
49
|
+
return w
|
|
50
|
+
|
|
51
|
+
# Try exact name match
|
|
52
|
+
for w in wlans:
|
|
53
|
+
name = w.get("name", "").lower()
|
|
54
|
+
if name == identifier_lower:
|
|
55
|
+
return w
|
|
56
|
+
|
|
57
|
+
# Try partial name match
|
|
58
|
+
for w in wlans:
|
|
59
|
+
name = w.get("name", "").lower()
|
|
60
|
+
if identifier_lower in name:
|
|
61
|
+
return w
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ========== WLAN Commands ==========
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("list")
|
|
70
|
+
def list_wlans(
|
|
71
|
+
output: Annotated[
|
|
72
|
+
OutputFormat,
|
|
73
|
+
typer.Option("--output", "-o", help="Output format"),
|
|
74
|
+
] = OutputFormat.TABLE,
|
|
75
|
+
verbose: Annotated[
|
|
76
|
+
bool,
|
|
77
|
+
typer.Option("--verbose", "-v", help="Show additional details"),
|
|
78
|
+
] = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""List all WLANs (wireless networks)."""
|
|
81
|
+
|
|
82
|
+
async def _list():
|
|
83
|
+
client = UniFiLocalClient()
|
|
84
|
+
return await client.get_wlans()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
wlans = run_with_spinner(_list(), "Fetching WLANs...")
|
|
88
|
+
except LocalAPIError as e:
|
|
89
|
+
print_error(str(e))
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
if not wlans:
|
|
93
|
+
console.print("[dim]No WLANs found[/dim]")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if output == OutputFormat.JSON:
|
|
97
|
+
output_json(wlans)
|
|
98
|
+
elif output == OutputFormat.CSV:
|
|
99
|
+
columns = [
|
|
100
|
+
("_id", "ID"),
|
|
101
|
+
("name", "Name"),
|
|
102
|
+
("security", "Security"),
|
|
103
|
+
("vlan", "VLAN"),
|
|
104
|
+
("enabled", "Enabled"),
|
|
105
|
+
]
|
|
106
|
+
csv_data = []
|
|
107
|
+
for w in wlans:
|
|
108
|
+
csv_data.append({
|
|
109
|
+
"_id": w.get("_id", ""),
|
|
110
|
+
"name": w.get("name", ""),
|
|
111
|
+
"security": get_security_type(w),
|
|
112
|
+
"vlan": w.get("vlan", ""),
|
|
113
|
+
"enabled": "Yes" if w.get("enabled", True) else "No",
|
|
114
|
+
})
|
|
115
|
+
output_csv(csv_data, columns)
|
|
116
|
+
else:
|
|
117
|
+
from rich.table import Table
|
|
118
|
+
|
|
119
|
+
table = Table(title="WLANs", show_header=True, header_style="bold cyan")
|
|
120
|
+
table.add_column("ID", style="dim")
|
|
121
|
+
table.add_column("Name")
|
|
122
|
+
table.add_column("Security")
|
|
123
|
+
table.add_column("VLAN")
|
|
124
|
+
table.add_column("Enabled")
|
|
125
|
+
if verbose:
|
|
126
|
+
table.add_column("Band")
|
|
127
|
+
table.add_column("Hide SSID")
|
|
128
|
+
table.add_column("PMF")
|
|
129
|
+
|
|
130
|
+
for w in wlans:
|
|
131
|
+
wlan_id = w.get("_id", "")
|
|
132
|
+
name = w.get("name", "")
|
|
133
|
+
security = get_security_type(w)
|
|
134
|
+
vlan = str(w.get("vlan", "-")) if w.get("vlan") else "-"
|
|
135
|
+
enabled = "[green]Yes[/green]" if w.get("enabled", True) else "[red]No[/red]"
|
|
136
|
+
|
|
137
|
+
if verbose:
|
|
138
|
+
# Band steering / radio settings
|
|
139
|
+
wlan_band = w.get("wlan_band", "both")
|
|
140
|
+
if wlan_band == "both":
|
|
141
|
+
band = "2.4/5 GHz"
|
|
142
|
+
elif wlan_band == "5g":
|
|
143
|
+
band = "5 GHz"
|
|
144
|
+
else:
|
|
145
|
+
band = "2.4 GHz"
|
|
146
|
+
|
|
147
|
+
hide_ssid = "Yes" if w.get("hide_ssid", False) else "No"
|
|
148
|
+
pmf = w.get("pmf_mode", "disabled")
|
|
149
|
+
pmf_map = {"disabled": "Off", "optional": "Optional", "required": "Required"}
|
|
150
|
+
pmf_display = pmf_map.get(pmf, pmf)
|
|
151
|
+
|
|
152
|
+
table.add_row(
|
|
153
|
+
wlan_id, name, security, vlan, enabled, band, hide_ssid, pmf_display
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
table.add_row(wlan_id, name, security, vlan, enabled)
|
|
157
|
+
|
|
158
|
+
console.print(table)
|
|
159
|
+
console.print(f"\n[dim]{len(wlans)} WLAN(s)[/dim]")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.command("get")
|
|
163
|
+
def get_wlan(
|
|
164
|
+
identifier: Annotated[str, typer.Argument(help="WLAN ID or name")],
|
|
165
|
+
output: Annotated[
|
|
166
|
+
OutputFormat,
|
|
167
|
+
typer.Option("--output", "-o", help="Output format"),
|
|
168
|
+
] = OutputFormat.TABLE,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Get detailed WLAN information."""
|
|
171
|
+
|
|
172
|
+
async def _get():
|
|
173
|
+
client = UniFiLocalClient()
|
|
174
|
+
wlans = await client.get_wlans()
|
|
175
|
+
return find_wlan(wlans, identifier)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
wlan = run_with_spinner(_get(), "Finding WLAN...")
|
|
179
|
+
except LocalAPIError as e:
|
|
180
|
+
print_error(str(e))
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
if not wlan:
|
|
184
|
+
print_error(f"WLAN '{identifier}' not found")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
if output == OutputFormat.JSON:
|
|
188
|
+
output_json(wlan)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Table output
|
|
192
|
+
from rich.table import Table
|
|
193
|
+
|
|
194
|
+
name = wlan.get("name", "Unknown")
|
|
195
|
+
console.print()
|
|
196
|
+
console.print(f"[bold cyan]WLAN: {name}[/bold cyan]")
|
|
197
|
+
console.print("-" * 40)
|
|
198
|
+
console.print()
|
|
199
|
+
|
|
200
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
201
|
+
table.add_column("Key", style="dim")
|
|
202
|
+
table.add_column("Value")
|
|
203
|
+
|
|
204
|
+
table.add_row("ID:", wlan.get("_id", ""))
|
|
205
|
+
table.add_row("Name:", name)
|
|
206
|
+
table.add_row("Security:", get_security_type(wlan))
|
|
207
|
+
enabled_display = "[green]Yes[/green]" if wlan.get("enabled", True) else "[red]No[/red]"
|
|
208
|
+
table.add_row("Enabled:", enabled_display)
|
|
209
|
+
table.add_row("", "")
|
|
210
|
+
|
|
211
|
+
# VLAN
|
|
212
|
+
vlan = wlan.get("vlan")
|
|
213
|
+
if vlan:
|
|
214
|
+
table.add_row("VLAN:", str(vlan))
|
|
215
|
+
|
|
216
|
+
# Network group
|
|
217
|
+
network_id = wlan.get("networkconf_id")
|
|
218
|
+
if network_id:
|
|
219
|
+
table.add_row("Network ID:", network_id)
|
|
220
|
+
|
|
221
|
+
table.add_row("", "")
|
|
222
|
+
|
|
223
|
+
# Radio settings
|
|
224
|
+
wlan_band = wlan.get("wlan_band", "both")
|
|
225
|
+
if wlan_band == "both":
|
|
226
|
+
table.add_row("Band:", "2.4 GHz & 5 GHz")
|
|
227
|
+
elif wlan_band == "5g":
|
|
228
|
+
table.add_row("Band:", "5 GHz only")
|
|
229
|
+
else:
|
|
230
|
+
table.add_row("Band:", "2.4 GHz only")
|
|
231
|
+
|
|
232
|
+
# Visibility
|
|
233
|
+
table.add_row("Hidden SSID:", "Yes" if wlan.get("hide_ssid", False) else "No")
|
|
234
|
+
|
|
235
|
+
# PMF
|
|
236
|
+
pmf = wlan.get("pmf_mode", "disabled")
|
|
237
|
+
pmf_map = {"disabled": "Disabled", "optional": "Optional", "required": "Required"}
|
|
238
|
+
pmf_display = pmf_map.get(pmf, pmf)
|
|
239
|
+
table.add_row("PMF Mode:", pmf_display)
|
|
240
|
+
|
|
241
|
+
# Fast roaming
|
|
242
|
+
table.add_row("Fast Roaming:", "Yes" if wlan.get("fast_roaming_enabled", False) else "No")
|
|
243
|
+
|
|
244
|
+
table.add_row("", "")
|
|
245
|
+
|
|
246
|
+
# Guest settings
|
|
247
|
+
if wlan.get("is_guest", False):
|
|
248
|
+
table.add_row("Guest Network:", "[yellow]Yes[/yellow]")
|
|
249
|
+
if wlan.get("guest_lan_isolation", False):
|
|
250
|
+
table.add_row("Client Isolation:", "Yes")
|
|
251
|
+
|
|
252
|
+
# Rate limiting
|
|
253
|
+
if wlan.get("usergroup_id"):
|
|
254
|
+
table.add_row("User Group:", wlan.get("usergroup_id", ""))
|
|
255
|
+
|
|
256
|
+
console.print(table)
|
|
257
|
+
console.print()
|
ui_cli/commands/mcp.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""MCP server management commands for Claude Desktop integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage MCP server for Claude Desktop")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==============================================================================
|
|
19
|
+
# Config Path Detection
|
|
20
|
+
# ==============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_config_path() -> Path:
|
|
24
|
+
"""Get Claude Desktop config path for current OS."""
|
|
25
|
+
system = platform.system()
|
|
26
|
+
if system == "Darwin": # macOS
|
|
27
|
+
return Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
|
|
28
|
+
elif system == "Windows":
|
|
29
|
+
return Path.home() / "AppData/Roaming/Claude/claude_desktop_config.json"
|
|
30
|
+
else: # Linux
|
|
31
|
+
return Path.home() / ".config/Claude/claude_desktop_config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_src_path() -> Path:
|
|
35
|
+
"""Get src directory (parent of ui_cli)."""
|
|
36
|
+
return Path(__file__).parent.parent.parent
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_project_root() -> Path:
|
|
40
|
+
"""Get project root directory (contains .env file)."""
|
|
41
|
+
return Path(__file__).parent.parent.parent.parent
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_ui_mcp_path() -> Path:
|
|
45
|
+
"""Get ui_mcp package path."""
|
|
46
|
+
return get_src_path() / "ui_mcp"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ==============================================================================
|
|
50
|
+
# Output Helpers
|
|
51
|
+
# ==============================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def print_header(title: str) -> None:
|
|
55
|
+
"""Print section header."""
|
|
56
|
+
console.print(f"\n[bold]{title}[/bold]")
|
|
57
|
+
console.print("=" * len(title))
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_success(msg: str) -> None:
|
|
62
|
+
"""Print success message."""
|
|
63
|
+
console.print(f"[green]✓[/green] {msg}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_warning(msg: str) -> None:
|
|
67
|
+
"""Print warning message."""
|
|
68
|
+
console.print(f"[yellow]![/yellow] {msg}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def print_error(msg: str) -> None:
|
|
72
|
+
"""Print error message."""
|
|
73
|
+
console.print(f"[red]✗[/red] {msg}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def print_info(msg: str) -> None:
|
|
77
|
+
"""Print info message."""
|
|
78
|
+
console.print(f"[dim]→[/dim] {msg}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def print_config_summary(config: dict) -> None:
|
|
82
|
+
"""Print MCP server configuration summary."""
|
|
83
|
+
console.print()
|
|
84
|
+
console.print("[dim]Configuration:[/dim]")
|
|
85
|
+
console.print(f" command: [cyan]{config.get('command', 'N/A')}[/cyan]")
|
|
86
|
+
args = config.get('args', [])
|
|
87
|
+
if args:
|
|
88
|
+
console.print(f" args: [cyan]{args}[/cyan]")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ==============================================================================
|
|
92
|
+
# Config Read/Write
|
|
93
|
+
# ==============================================================================
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_config(path: Path) -> dict:
|
|
97
|
+
"""Read config file, return empty dict structure if doesn't exist."""
|
|
98
|
+
if not path.exists():
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
content = path.read_text()
|
|
103
|
+
if not content.strip():
|
|
104
|
+
return {}
|
|
105
|
+
return json.loads(content)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
raise typer.Exit(
|
|
108
|
+
console.print(f"[red]✗[/red] Invalid JSON in config file: {e}\n"
|
|
109
|
+
f" Please fix manually or delete: {path}")
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def write_config(path: Path, config: dict) -> None:
|
|
114
|
+
"""Write config file with proper formatting."""
|
|
115
|
+
# Ensure parent directory exists
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Write with nice formatting
|
|
119
|
+
path.write_text(json.dumps(config, indent=2) + "\n")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def create_backup(path: Path) -> Path | None:
|
|
123
|
+
"""Create timestamped backup of config file."""
|
|
124
|
+
if not path.exists():
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
128
|
+
backup_path = path.with_suffix(f".{timestamp}.bak")
|
|
129
|
+
backup_path.write_text(path.read_text())
|
|
130
|
+
return backup_path
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ==============================================================================
|
|
134
|
+
# MCP Config Generation
|
|
135
|
+
# ==============================================================================
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_wrapper_script() -> Path:
|
|
139
|
+
"""Get path to MCP server wrapper script."""
|
|
140
|
+
return get_project_root() / "scripts" / "mcp-server.sh"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def generate_mcp_config() -> dict:
|
|
144
|
+
"""Generate ui-cli MCP server configuration.
|
|
145
|
+
|
|
146
|
+
Uses a wrapper script that:
|
|
147
|
+
- Changes to project root
|
|
148
|
+
- Sources .env file to load credentials
|
|
149
|
+
- Sets PYTHONPATH
|
|
150
|
+
- Runs the MCP server with the specified Python
|
|
151
|
+
"""
|
|
152
|
+
wrapper_script = get_wrapper_script()
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"command": str(wrapper_script),
|
|
156
|
+
"args": [],
|
|
157
|
+
"env": {
|
|
158
|
+
"PYTHON_PATH": sys.executable,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_mcp_module(python_path: str | Path) -> tuple[bool, str]:
|
|
164
|
+
"""Check if mcp module is installed in the given Python environment.
|
|
165
|
+
|
|
166
|
+
Returns (success, message) tuple.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
result = subprocess.run(
|
|
170
|
+
[str(python_path), "-c", "import mcp.server.fastmcp; print('ok')"],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
timeout=10,
|
|
174
|
+
)
|
|
175
|
+
if result.returncode == 0 and "ok" in result.stdout:
|
|
176
|
+
return True, "installed"
|
|
177
|
+
else:
|
|
178
|
+
return False, "not installed"
|
|
179
|
+
except subprocess.TimeoutExpired:
|
|
180
|
+
return False, "check timed out"
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return False, str(e)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ==============================================================================
|
|
186
|
+
# Commands
|
|
187
|
+
# ==============================================================================
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command()
|
|
191
|
+
def install(
|
|
192
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing configuration"),
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Install ui-cli MCP server to Claude Desktop."""
|
|
195
|
+
print_header("UI-CLI MCP Server Installation")
|
|
196
|
+
|
|
197
|
+
config_path = get_config_path()
|
|
198
|
+
console.print(f"Config file: [cyan]{config_path}[/cyan]\n")
|
|
199
|
+
|
|
200
|
+
# Read existing config
|
|
201
|
+
config = read_config(config_path)
|
|
202
|
+
|
|
203
|
+
# Check existing MCP servers
|
|
204
|
+
mcp_servers = config.get("mcpServers", {})
|
|
205
|
+
existing_count = len(mcp_servers)
|
|
206
|
+
|
|
207
|
+
if config_path.exists():
|
|
208
|
+
print_success(f"Config file found ({existing_count} existing MCP server(s))")
|
|
209
|
+
else:
|
|
210
|
+
print_info("Config file will be created")
|
|
211
|
+
|
|
212
|
+
# Check wrapper script exists
|
|
213
|
+
wrapper_script = get_wrapper_script()
|
|
214
|
+
if not wrapper_script.exists():
|
|
215
|
+
print_error(f"Wrapper script not found: {wrapper_script}")
|
|
216
|
+
console.print(" Run from the ui-cli project directory")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
print_success(f"Wrapper script found: {wrapper_script}")
|
|
219
|
+
|
|
220
|
+
# Check if ui-cli already configured
|
|
221
|
+
if "ui-cli" in mcp_servers:
|
|
222
|
+
if not force:
|
|
223
|
+
print_warning("'ui-cli' already configured. Use --force to update.")
|
|
224
|
+
print_config_summary(mcp_servers["ui-cli"])
|
|
225
|
+
raise typer.Exit(1)
|
|
226
|
+
else:
|
|
227
|
+
print_info("Updating existing 'ui-cli' configuration")
|
|
228
|
+
|
|
229
|
+
# Create backup
|
|
230
|
+
if config_path.exists():
|
|
231
|
+
backup_path = create_backup(config_path)
|
|
232
|
+
if backup_path:
|
|
233
|
+
print_success(f"Backup created: {backup_path.name}")
|
|
234
|
+
|
|
235
|
+
# Add ui-cli config
|
|
236
|
+
if "mcpServers" not in config:
|
|
237
|
+
config["mcpServers"] = {}
|
|
238
|
+
|
|
239
|
+
mcp_config = generate_mcp_config()
|
|
240
|
+
config["mcpServers"]["ui-cli"] = mcp_config
|
|
241
|
+
|
|
242
|
+
# Write config
|
|
243
|
+
write_config(config_path, config)
|
|
244
|
+
print_success("Added 'ui-cli' to mcpServers")
|
|
245
|
+
|
|
246
|
+
# Print summary
|
|
247
|
+
print_config_summary(mcp_config)
|
|
248
|
+
|
|
249
|
+
# Check mcp module
|
|
250
|
+
console.print()
|
|
251
|
+
mcp_ok, mcp_msg = check_mcp_module(sys.executable)
|
|
252
|
+
if mcp_ok:
|
|
253
|
+
print_success(f"mcp package installed ({mcp_msg})")
|
|
254
|
+
else:
|
|
255
|
+
print_error(f"mcp package not installed in {sys.executable}")
|
|
256
|
+
console.print()
|
|
257
|
+
console.print("[bold red]Required:[/bold red] Install the mcp package:")
|
|
258
|
+
console.print(f" [cyan]{sys.executable} -m pip install mcp[/cyan]")
|
|
259
|
+
console.print()
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
console.print()
|
|
263
|
+
console.print("[bold yellow]Restart Claude Desktop to activate the MCP server.[/bold yellow]")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.command()
|
|
267
|
+
def check() -> None:
|
|
268
|
+
"""Check MCP server installation status."""
|
|
269
|
+
print_header("UI-CLI MCP Server Status")
|
|
270
|
+
|
|
271
|
+
config_path = get_config_path()
|
|
272
|
+
console.print(f"Config file: [cyan]{config_path}[/cyan]\n")
|
|
273
|
+
|
|
274
|
+
# Check config file exists
|
|
275
|
+
if not config_path.exists():
|
|
276
|
+
print_error("Config file not found - Claude Desktop may not be installed")
|
|
277
|
+
raise typer.Exit(1)
|
|
278
|
+
|
|
279
|
+
print_success("Config file exists")
|
|
280
|
+
|
|
281
|
+
# Read config
|
|
282
|
+
config = read_config(config_path)
|
|
283
|
+
mcp_servers = config.get("mcpServers", {})
|
|
284
|
+
|
|
285
|
+
# Check ui-cli configured
|
|
286
|
+
if "ui-cli" not in mcp_servers:
|
|
287
|
+
print_error("'ui-cli' not configured - run 'ui mcp install'")
|
|
288
|
+
raise typer.Exit(1)
|
|
289
|
+
|
|
290
|
+
print_success("'ui-cli' is configured")
|
|
291
|
+
|
|
292
|
+
ui_cli_config = mcp_servers["ui-cli"]
|
|
293
|
+
has_errors = False
|
|
294
|
+
|
|
295
|
+
# Validate wrapper script
|
|
296
|
+
command_path = Path(ui_cli_config.get("command", ""))
|
|
297
|
+
if command_path.exists():
|
|
298
|
+
print_success(f"Wrapper script found: {command_path}")
|
|
299
|
+
else:
|
|
300
|
+
print_error(f"Wrapper script not found: {command_path}")
|
|
301
|
+
has_errors = True
|
|
302
|
+
|
|
303
|
+
# Check project root and .env
|
|
304
|
+
project_root = get_project_root()
|
|
305
|
+
if project_root.exists():
|
|
306
|
+
print_success(f"Project root valid: {project_root}")
|
|
307
|
+
env_file = project_root / ".env"
|
|
308
|
+
if env_file.exists():
|
|
309
|
+
print_success(".env file found")
|
|
310
|
+
else:
|
|
311
|
+
print_warning(f".env file not found at {env_file}")
|
|
312
|
+
else:
|
|
313
|
+
print_error(f"Project root not found: {project_root}")
|
|
314
|
+
has_errors = True
|
|
315
|
+
|
|
316
|
+
# Check ui_mcp module
|
|
317
|
+
ui_mcp_path = get_ui_mcp_path()
|
|
318
|
+
if ui_mcp_path.exists() and (ui_mcp_path / "__init__.py").exists():
|
|
319
|
+
print_success("ui_mcp module found")
|
|
320
|
+
else:
|
|
321
|
+
print_error(f"ui_mcp module not found at: {ui_mcp_path}")
|
|
322
|
+
has_errors = True
|
|
323
|
+
|
|
324
|
+
# Check mcp package installed
|
|
325
|
+
mcp_ok, mcp_msg = check_mcp_module(sys.executable)
|
|
326
|
+
if mcp_ok:
|
|
327
|
+
print_success(f"mcp package installed ({mcp_msg})")
|
|
328
|
+
else:
|
|
329
|
+
print_error("mcp package not installed")
|
|
330
|
+
console.print(f" Run: [cyan]{sys.executable} -m pip install mcp[/cyan]")
|
|
331
|
+
has_errors = True
|
|
332
|
+
|
|
333
|
+
# List other servers
|
|
334
|
+
other_servers = [k for k in mcp_servers.keys() if k != "ui-cli"]
|
|
335
|
+
if other_servers:
|
|
336
|
+
console.print()
|
|
337
|
+
console.print(f"[dim]Other MCP servers: {', '.join(other_servers)}[/dim]")
|
|
338
|
+
|
|
339
|
+
console.print()
|
|
340
|
+
if has_errors:
|
|
341
|
+
console.print("[bold red]Status: Not Ready (fix errors above)[/bold red]")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
else:
|
|
344
|
+
console.print("[bold green]Status: Ready[/bold green]")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@app.command()
|
|
348
|
+
def remove() -> None:
|
|
349
|
+
"""Remove ui-cli MCP server from Claude Desktop."""
|
|
350
|
+
print_header("UI-CLI MCP Server Removal")
|
|
351
|
+
|
|
352
|
+
config_path = get_config_path()
|
|
353
|
+
console.print(f"Config file: [cyan]{config_path}[/cyan]\n")
|
|
354
|
+
|
|
355
|
+
# Check config exists
|
|
356
|
+
if not config_path.exists():
|
|
357
|
+
print_error("Config file not found")
|
|
358
|
+
raise typer.Exit(1)
|
|
359
|
+
|
|
360
|
+
# Read config
|
|
361
|
+
config = read_config(config_path)
|
|
362
|
+
mcp_servers = config.get("mcpServers", {})
|
|
363
|
+
|
|
364
|
+
# Check ui-cli exists
|
|
365
|
+
if "ui-cli" not in mcp_servers:
|
|
366
|
+
print_warning("'ui-cli' is not configured - nothing to remove")
|
|
367
|
+
raise typer.Exit(0)
|
|
368
|
+
|
|
369
|
+
# Create backup
|
|
370
|
+
backup_path = create_backup(config_path)
|
|
371
|
+
if backup_path:
|
|
372
|
+
print_success(f"Backup created: {backup_path.name}")
|
|
373
|
+
|
|
374
|
+
# Remove ui-cli
|
|
375
|
+
del config["mcpServers"]["ui-cli"]
|
|
376
|
+
print_success("Removed 'ui-cli' from mcpServers")
|
|
377
|
+
|
|
378
|
+
# Write config
|
|
379
|
+
write_config(config_path, config)
|
|
380
|
+
|
|
381
|
+
# List remaining servers
|
|
382
|
+
remaining = list(config.get("mcpServers", {}).keys())
|
|
383
|
+
if remaining:
|
|
384
|
+
console.print()
|
|
385
|
+
console.print(f"[dim]Remaining MCP servers: {', '.join(remaining)}[/dim]")
|
|
386
|
+
else:
|
|
387
|
+
console.print()
|
|
388
|
+
console.print("[dim]No MCP servers remaining[/dim]")
|
|
389
|
+
|
|
390
|
+
console.print()
|
|
391
|
+
console.print("[bold yellow]Restart Claude Desktop to apply changes.[/bold yellow]")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@app.command()
|
|
395
|
+
def show() -> None:
|
|
396
|
+
"""Show current Claude Desktop MCP configuration."""
|
|
397
|
+
print_header("Claude Desktop MCP Configuration")
|
|
398
|
+
|
|
399
|
+
config_path = get_config_path()
|
|
400
|
+
console.print(f"Config file: [cyan]{config_path}[/cyan]\n")
|
|
401
|
+
|
|
402
|
+
if not config_path.exists():
|
|
403
|
+
print_error("Config file not found")
|
|
404
|
+
raise typer.Exit(1)
|
|
405
|
+
|
|
406
|
+
# Read and display config
|
|
407
|
+
config = read_config(config_path)
|
|
408
|
+
|
|
409
|
+
if not config:
|
|
410
|
+
console.print("[dim]Config file is empty[/dim]")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Pretty print with syntax highlighting
|
|
414
|
+
json_str = json.dumps(config, indent=2)
|
|
415
|
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
|
416
|
+
console.print(syntax)
|