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
@@ -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)