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,46 @@
1
+ ui_cli/__init__.py,sha256=ElAkVLqmlJpn6L_aK_TtTFzckYBjgET0lPq-0An6xn0,999
2
+ ui_cli/client.py,sha256=mDM56BZbDsiOGrk0gQd-whAMQTQWUstOcSMaIukI2c0,9753
3
+ ui_cli/config.py,sha256=GKasdbPeSAobbRx6f5d89hIfod_8ycpgLR5T5gFC3Mo,3212
4
+ ui_cli/groups.py,sha256=cb6W3pVxf9pTVmozY9yWkwJPZ5olh0djs-G8HZ277DU,18786
5
+ ui_cli/local_client.py,sha256=IOJEOONgtrE3kmOCvp1jtqziALyOzcCrVEitRBgKQKM,33855
6
+ ui_cli/main.py,sha256=h__Koxw2dB6ZMq5c7pDIf9142BuUgy-GXgZIjVDFGA4,1593
7
+ ui_cli/models.py,sha256=e_RzeVDRrhKZFtH38MhtjfcwUgtgAxooWYad4rdU7iQ,5394
8
+ ui_cli/output.py,sha256=MzsLj8LoWlK8sjaQ4qJF3VESAZaoJiE1ny0yj62YI10,7267
9
+ ui_cli/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
10
+ ui_cli/commands/devices.py,sha256=vaJuln0gapuytX_sqXGk_haguOnQvYvD4ob5ct-RqBA,4689
11
+ ui_cli/commands/groups.py,sha256=Ol4gY16uzAvyoa6Ltv4-RXUhOpDpOBVATGB4rd3jhuk,16668
12
+ ui_cli/commands/hosts.py,sha256=mD8hq8Z7uweVJ7FeVylJONb282_YTXCVaaCEC-UyzK4,2749
13
+ ui_cli/commands/isp.py,sha256=GCy2GdxU8tAhVjsjVQuiCBuAUPnWts2O_p6hjTUR8Q8,2524
14
+ ui_cli/commands/mcp.py,sha256=55IQ63X-4OWAw3foTQpuhpTx36u9s3gidlQVUP52Bm8,12749
15
+ ui_cli/commands/sdwan.py,sha256=dVR626mSKU2QFFohPZ7-Hr-8lEXPbY6UBBCC7oy9El8,4157
16
+ ui_cli/commands/sites.py,sha256=ku-_tTJkkKTDiUntQnSk3DlMEHLKdMBOtAb-Y55EaPw,1535
17
+ ui_cli/commands/speedtest.py,sha256=ZuE_mYvpFdq4iDhDY6hUUe9ueAdnZZtlys2ToDMP_7k,5929
18
+ ui_cli/commands/status.py,sha256=LBnLTc5xZjoTu4u_ZOStvipQUKySaHkfSsr0GoFH-gE,13955
19
+ ui_cli/commands/version.py,sha256=wQXH3XevQZ9ModtrUohDdHZ_mPyBiMmQ6NEyZUP288s,289
20
+ ui_cli/commands/local/__init__.py,sha256=fow1tALkbwnA6O840hKVpw-t7vslaA6sAvc4i6TTlTA,1927
21
+ ui_cli/commands/local/apgroups.py,sha256=K8JeU4mqI9i9VtqCwwb47W03ddwA-Y6dkiba1q_fbG8,13305
22
+ ui_cli/commands/local/clients.py,sha256=uqHPCNbOOKIEktIJAi-yNWlN3kY6bvgb6hxa7PjyKFI,52192
23
+ ui_cli/commands/local/config.py,sha256=_-O2YUdXcY4HzSGjESLg40Kwt9n8bjWcw7IjmOp9LbA,29259
24
+ ui_cli/commands/local/devices.py,sha256=KObxnioTlk1Di-MMWSWyEIwNcGYJ9oXGU1ZkEXa2N3o,17075
25
+ ui_cli/commands/local/dpi.py,sha256=ThaEACK8iaE7NJx5f6FUMOMav7j9jlmVyaTMUSFQs58,10613
26
+ ui_cli/commands/local/events.py,sha256=lsJGUBYqOtxMQiOuPV3M8B2oxY8X6_tflTqxENudPpo,8696
27
+ ui_cli/commands/local/firewall.py,sha256=bA3MyJFrqVQpKSj88PIEpiTbGw04IMYw3Pk_t4D_mbk,8785
28
+ ui_cli/commands/local/health.py,sha256=Z-kJx_ZQrywnORLQfNoqYvGgTiu3bb2JmPDTKTB9qN8,6680
29
+ ui_cli/commands/local/networks.py,sha256=Teyf7BixuFaVQ3crRUhBfoHKLyyulpMAiPeNbak1T68,14087
30
+ ui_cli/commands/local/portfwd.py,sha256=CmZ88UEpdfwDRBAsOg_VOhLvqXYjrPFE01gLLriuTAg,4501
31
+ ui_cli/commands/local/stats.py,sha256=6A683uz16a9-fXlad1RGvD_WiHPzZMbm2U6OmxRrMmc,7274
32
+ ui_cli/commands/local/utils.py,sha256=-sowKlYXCIiScaOXs45EVXAwUPe7aPzpzhQm6BPHXSI,2203
33
+ ui_cli/commands/local/vouchers.py,sha256=TuojX6q_02nnHyF2P_UYnBCpSz3RvvZ3F_3PsU0Shug,11935
34
+ ui_cli/commands/local/wan.py,sha256=3RzeHKuemvQ-XWyW-0AfvlI4CiK6soTDeZBvG1UiMTE,9574
35
+ ui_cli/commands/local/wlans.py,sha256=88pnv1V5kE8tN1PF3ux8jYw0L-bGje0Gkxr7WJdcXb4,7577
36
+ ui_mcp/ARCHITECTURE.md,sha256=LWjvPn-YFppCdWN2s1cF4ayfe0WbkVGbFNLXb70thEI,7654
37
+ ui_mcp/README.md,sha256=IE_ughPVMrabxh5zJVWkl4lfHBymKrmW4usGGpJR04M,6322
38
+ ui_mcp/__init__.py,sha256=7nDXTY6bZDjiO2VMNNb5dMmtqJBW3ETpolCN7Z13Grc,192
39
+ ui_mcp/__main__.py,sha256=SyBy7DcPJg7Ez1896oFy5-PehWxcMMwCPW4XYGCt_uQ,147
40
+ ui_mcp/cli_runner.py,sha256=pZf9GtF8-N1UCRHPwwVp7nFdDw5rqKvo5FDWhENxDiY,3498
41
+ ui_mcp/server.py,sha256=RtKjOzfs5PbTfzTaSov3Ex_rJA-VlPsBkwBphKGMNmU,13248
42
+ ui_cli-1.2.1.dist-info/METADATA,sha256=ynwy0qVWWDYk9SrlPqrYftaPPeuQxOngRFQ5vJcWeWg,39465
43
+ ui_cli-1.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
44
+ ui_cli-1.2.1.dist-info/entry_points.txt,sha256=h9ftzR21tqilO9dmjFTI_TZ9o8IdP_OdEBsNSc0AMb4,67
45
+ ui_cli-1.2.1.dist-info/licenses/LICENSE,sha256=zjnj_JPLbOweq-uYSCSb8KympcCNzxZQS0ndAaaqEMQ,1072
46
+ ui_cli-1.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ui = ui_cli.main:app
3
+ ui-mcp = ui_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vedanta Barooah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ui_mcp/ARCHITECTURE.md ADDED
@@ -0,0 +1,243 @@
1
+ # MCP Server Architecture
2
+
3
+ ## Design Principles
4
+
5
+ ### 1. CLI as Source of Truth
6
+
7
+ The MCP server is a **thin wrapper** around the UI CLI. All business logic, API calls, and formatting live in the CLI. This ensures:
8
+
9
+ - Consistent behavior between terminal and Claude Desktop
10
+ - Single place to fix bugs
11
+ - Easy to test (just run the CLI)
12
+
13
+ ### 2. Friendly Tool Names
14
+
15
+ Tools use natural names that map to user intent:
16
+
17
+ | User Intent | Tool Name | CLI Command |
18
+ |-------------|-----------|-------------|
19
+ | "How many clients?" | `client_count` | `./ui lo clients count` |
20
+ | "Block a device" | `block_client` | `./ui lo clients block` |
21
+ | "Network health" | `network_health` | `./ui lo health` |
22
+
23
+ ### 3. Structured Responses
24
+
25
+ All tools return JSON with a consistent structure:
26
+
27
+ ```json
28
+ {
29
+ "summary": "Human-readable one-liner for Claude",
30
+ "data": { ... },
31
+ "count": 10
32
+ }
33
+ ```
34
+
35
+ This helps Claude generate natural responses without dumping raw JSON.
36
+
37
+ ## Component Details
38
+
39
+ ### FastMCP Server (`server.py`)
40
+
41
+ ```python
42
+ from mcp.server.fastmcp import FastMCP
43
+
44
+ server = FastMCP(
45
+ "ui-cli",
46
+ instructions="Manage UniFi network infrastructure"
47
+ )
48
+
49
+ @server.tool()
50
+ async def network_health() -> str:
51
+ """Tool docstring becomes the description Claude sees."""
52
+ result = run_cli(["lo", "health"])
53
+ return format_result(result, "Health summary")
54
+ ```
55
+
56
+ Key points:
57
+ - Uses official Anthropic MCP SDK (`mcp.server.fastmcp`)
58
+ - Tools are async but call sync subprocess
59
+ - Returns JSON strings (FastMCP requirement)
60
+ - Docstrings are shown to Claude as tool descriptions
61
+
62
+ ### CLI Runner (`cli_runner.py`)
63
+
64
+ ```python
65
+ def run_cli(args: list[str], timeout: int = 30) -> dict:
66
+ """Execute UI CLI and return parsed JSON."""
67
+
68
+ # Use same Python as MCP server (conda env)
69
+ python_path = sys.executable
70
+ cmd = [python_path, "-m", "ui_cli.main"] + args
71
+
72
+ # Auto-add JSON output flag
73
+ if "-o" not in args:
74
+ cmd.extend(["-o", "json"])
75
+
76
+ # Auto-add -y for actions (skip confirmation)
77
+ if any(action in args for action in ["block", "restart"]):
78
+ cmd.append("-y")
79
+
80
+ result = subprocess.run(cmd, capture_output=True, ...)
81
+ return json.loads(result.stdout)
82
+ ```
83
+
84
+ Key points:
85
+ - Uses `sys.executable` to ensure correct conda Python
86
+ - Auto-adds `--output json` flag
87
+ - Auto-adds `-y` flag for action commands
88
+ - Handles timeouts and errors gracefully
89
+
90
+ ### Entry Point (`__main__.py`)
91
+
92
+ ```python
93
+ from ui_mcp.server import main
94
+
95
+ if __name__ == "__main__":
96
+ main()
97
+ ```
98
+
99
+ Allows running as: `python -m ui_mcp`
100
+
101
+ ### Wrapper Script (`scripts/mcp-server.sh`)
102
+
103
+ ```bash
104
+ #!/bin/bash
105
+ # Load .env for credentials
106
+ source .env
107
+
108
+ # Set PYTHONPATH
109
+ export PYTHONPATH="$PROJECT_ROOT/src"
110
+
111
+ # Run with specified Python
112
+ exec "$PYTHON" -m ui_mcp "$@"
113
+ ```
114
+
115
+ Claude Desktop calls this script, which:
116
+ 1. Changes to project directory
117
+ 2. Loads `.env` file (API credentials)
118
+ 3. Sets `PYTHONPATH` for imports
119
+ 4. Runs the MCP server
120
+
121
+ ## Data Flow
122
+
123
+ ### Read Operation (e.g., `client_count`)
124
+
125
+ ```
126
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
127
+ │ Claude │ │ FastMCP │ │ CLI Runner │ │ UI CLI │
128
+ │ Desktop │ │ Server │ │ │ │ │
129
+ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
130
+ │ │ │ │
131
+ │ MCP: client_count │ │ │
132
+ │──────────────────>│ │ │
133
+ │ │ │ │
134
+ │ │ run_cli(["lo", │ │
135
+ │ │ "clients","count"]) │
136
+ │ │──────────────────>│ │
137
+ │ │ │ │
138
+ │ │ │ subprocess: │
139
+ │ │ │ python -m ui_cli │
140
+ │ │ │ lo clients count │
141
+ │ │ │ -o json │
142
+ │ │ │──────────────────>│
143
+ │ │ │ │
144
+ │ │ │ {"counts": │
145
+ │ │ │ {"Wired":17, │
146
+ │ │ │ "Wireless":70}│
147
+ │ │ │<──────────────────│
148
+ │ │ │ │
149
+ │ │ {"summary": "...",│ │
150
+ │ │ "counts": {...}} │ │
151
+ │ │<──────────────────│ │
152
+ │ │ │ │
153
+ │ {"summary": │ │ │
154
+ │ "Total: 87..."} │ │ │
155
+ │<──────────────────│ │ │
156
+ │ │ │ │
157
+ ```
158
+
159
+ ### Write Operation (e.g., `block_client`)
160
+
161
+ Same flow, but:
162
+ 1. CLI Runner adds `-y` flag to skip confirmation
163
+ 2. CLI returns `{"success": true, "action": "blocked", ...}`
164
+ 3. Summary becomes "Blocked client: iPhone"
165
+
166
+ ## Error Handling
167
+
168
+ ### CLI Errors
169
+
170
+ ```python
171
+ # CLI returns non-zero exit code
172
+ {
173
+ "error": True,
174
+ "message": "Client not found: xyz",
175
+ "exit_code": 1
176
+ }
177
+ ```
178
+
179
+ ### Timeout Errors
180
+
181
+ ```python
182
+ # Command exceeds timeout
183
+ {
184
+ "error": True,
185
+ "message": "Command timed out after 30s"
186
+ }
187
+ ```
188
+
189
+ ### API Errors
190
+
191
+ ```python
192
+ # UniFi API returns error
193
+ {
194
+ "error": True,
195
+ "message": "Authentication failed: Invalid API key"
196
+ }
197
+ ```
198
+
199
+ All errors include `"error": True` so Claude can respond appropriately.
200
+
201
+ ## Security Considerations
202
+
203
+ ### Credentials
204
+
205
+ - Stored in `.env` file (not in repo)
206
+ - Loaded by wrapper script before MCP server starts
207
+ - Never exposed in tool responses
208
+
209
+ ### Action Confirmation
210
+
211
+ - CLI normally prompts for confirmation on destructive actions
212
+ - MCP server adds `-y` flag to skip prompts
213
+ - Claude should confirm with user before calling action tools
214
+
215
+ ### Network Access
216
+
217
+ - Local controller access via HTTPS (self-signed cert OK)
218
+ - Cloud API access via `api.ui.com`
219
+ - No inbound connections required
220
+
221
+ ## Performance
222
+
223
+ ### Typical Response Times
224
+
225
+ | Operation | Time |
226
+ |-----------|------|
227
+ | `network_health` | ~500ms |
228
+ | `client_count` | ~800ms |
229
+ | `device_list` | ~1s |
230
+ | `run_speedtest` | 30-60s |
231
+
232
+ ### Optimization
233
+
234
+ - CLI caches controller connection
235
+ - JSON output avoids table rendering overhead
236
+ - Subprocess overhead is minimal (~50ms)
237
+
238
+ ## Future Improvements
239
+
240
+ 1. **Caching** - Cache expensive queries like device list
241
+ 2. **Streaming** - Stream progress for long operations
242
+ 3. **Batch operations** - Block/unblock multiple clients
243
+ 4. **Webhooks** - React to network events
ui_mcp/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # UI-CLI MCP Server
2
+
3
+ Model Context Protocol (MCP) server for managing UniFi network infrastructure through Claude Desktop.
4
+
5
+ ## Overview
6
+
7
+ This MCP server exposes UniFi management tools to Claude Desktop, allowing natural language interaction with your network. Ask questions like:
8
+
9
+ - "How many devices are connected to my network?"
10
+ - "What's my network health status?"
11
+ - "Block the kids iPad"
12
+ - "Restart the garage access point"
13
+
14
+ ## Architecture
15
+
16
+ ```mermaid
17
+ flowchart TB
18
+ subgraph Claude Desktop
19
+ CD[Claude Desktop App]
20
+ end
21
+
22
+ subgraph MCP Server
23
+ FS[FastMCP Server<br/>server.py]
24
+ CR[CLI Runner<br/>cli_runner.py]
25
+ end
26
+
27
+ subgraph UI CLI
28
+ CLI[ui_cli.main]
29
+ LC[Local Client<br/>UniFi Controller API]
30
+ CC[Cloud Client<br/>UniFi Cloud API]
31
+ end
32
+
33
+ subgraph UniFi Infrastructure
34
+ UDM[UDM / Controller]
35
+ CLOUD[UniFi Cloud API]
36
+ end
37
+
38
+ CD <-->|MCP Protocol<br/>stdio| FS
39
+ FS --> CR
40
+ CR -->|subprocess<br/>python -m ui_cli.main| CLI
41
+ CLI --> LC
42
+ CLI --> CC
43
+ LC <-->|HTTPS| UDM
44
+ CC <-->|HTTPS| CLOUD
45
+ ```
46
+
47
+ ## How It Works
48
+
49
+ 1. **Claude Desktop** connects to the MCP server via stdio
50
+ 2. **FastMCP Server** (`server.py`) registers 21 tools with friendly names
51
+ 3. **CLI Runner** (`cli_runner.py`) executes `./ui` commands via subprocess
52
+ 4. **UI CLI** performs the actual API calls and returns JSON
53
+ 5. **Results** flow back through the chain to Claude
54
+
55
+ ### Why Subprocess?
56
+
57
+ The v2 architecture uses subprocess calls instead of direct Python imports because:
58
+
59
+ - **Single source of truth** - CLI handles all logic, formatting, error handling
60
+ - **Consistent behavior** - Same output as terminal usage
61
+ - **Easier debugging** - Test tools by running CLI directly
62
+ - **Simpler maintenance** - Add MCP tool = call existing CLI command
63
+
64
+ ## Installation
65
+
66
+ ### Prerequisites
67
+
68
+ - Python 3.11+ with conda environment `ui-cli`
69
+ - Claude Desktop installed
70
+ - UniFi credentials configured in `.env`
71
+
72
+ ### Setup
73
+
74
+ ```bash
75
+ # Install MCP server to Claude Desktop
76
+ ./ui mcp install
77
+
78
+ # Verify installation
79
+ ./ui mcp check
80
+
81
+ # View current config
82
+ ./ui mcp show
83
+ ```
84
+
85
+ ### Configuration
86
+
87
+ The installer adds this to Claude Desktop's config:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "ui-cli": {
93
+ "command": "/path/to/ui-cmd/scripts/mcp-server.sh",
94
+ "args": [],
95
+ "env": {
96
+ "PYTHON_PATH": "/path/to/conda/envs/ui-cli/bin/python"
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Available Tools
104
+
105
+ ### Status & Health
106
+
107
+ | Tool | Description | Example Prompt |
108
+ |------|-------------|----------------|
109
+ | `network_status` | Check API connectivity | "Is my network API working?" |
110
+ | `network_health` | Site health summary | "What's my network health?" |
111
+ | `internet_speed` | Last speed test result | "What's my internet speed?" |
112
+ | `run_speedtest` | Run new speed test | "Run a speed test" |
113
+ | `isp_performance` | ISP metrics over time | "How's my ISP been performing?" |
114
+
115
+ ### Counts & Lists
116
+
117
+ | Tool | Description | Example Prompt |
118
+ |------|-------------|----------------|
119
+ | `client_count` | Count clients by category | "How many devices are connected?" |
120
+ | `device_list` | List UniFi devices | "What UniFi devices do I have?" |
121
+ | `network_list` | List networks/VLANs | "Show my networks" |
122
+
123
+ ### Lookups
124
+
125
+ | Tool | Description | Example Prompt |
126
+ |------|-------------|----------------|
127
+ | `find_client` | Find client by name/MAC | "Find my iPhone" |
128
+ | `find_device` | Find device by name/MAC/IP | "Show the living room AP" |
129
+ | `client_status` | Check if client is online/blocked | "Is the TV online?" |
130
+
131
+ ### Actions
132
+
133
+ | Tool | Description | Example Prompt |
134
+ |------|-------------|----------------|
135
+ | `block_client` | Block from network | "Block the kids iPad" |
136
+ | `unblock_client` | Restore access | "Unblock the kids iPad" |
137
+ | `kick_client` | Force disconnect | "Disconnect my laptop" |
138
+ | `restart_device` | Reboot device | "Restart the garage AP" |
139
+ | `create_voucher` | Create guest WiFi code | "Create a guest WiFi voucher" |
140
+
141
+ ### Groups
142
+
143
+ | Tool | Description | Example Prompt |
144
+ |------|-------------|----------------|
145
+ | `list_groups` | List all client groups | "What groups do I have?" |
146
+ | `get_group` | Get group details | "Show the kids devices group" |
147
+ | `block_group` | Block all clients in group | "Block all kids devices" |
148
+ | `unblock_group` | Unblock all clients in group | "Unblock the kids devices" |
149
+ | `group_status` | Live status of group members | "Are the kids devices online?" |
150
+
151
+ ## File Structure
152
+
153
+ ```
154
+ src/ui_mcp/
155
+ ├── __init__.py # Package init, version
156
+ ├── __main__.py # Entry point: python -m ui_mcp
157
+ ├── server.py # FastMCP server + 16 tool definitions
158
+ ├── cli_runner.py # Subprocess wrapper for CLI calls
159
+ └── README.md # This file
160
+ ```
161
+
162
+ ## Development
163
+
164
+ ### Testing Tools
165
+
166
+ Run the test script to validate all tools:
167
+
168
+ ```bash
169
+ python scripts/test-mcp-tools.py
170
+ ```
171
+
172
+ ### Adding a New Tool
173
+
174
+ 1. Add CLI command with `--json` output support
175
+ 2. Add tool function in `server.py`:
176
+
177
+ ```python
178
+ @server.tool()
179
+ async def my_new_tool(param: str) -> str:
180
+ """Tool description shown to Claude.
181
+
182
+ Args:
183
+ param: Parameter description
184
+ """
185
+ result = run_cli(["lo", "mycommand", param])
186
+ if "error" in result:
187
+ return format_result(result)
188
+ return format_result(result, "Human-readable summary")
189
+ ```
190
+
191
+ 3. Test with `python scripts/test-mcp-tools.py`
192
+
193
+ ### Debugging
194
+
195
+ Check MCP server logs in Claude Desktop:
196
+ - macOS: `~/Library/Logs/Claude/mcp*.log`
197
+
198
+ Test CLI runner directly:
199
+
200
+ ```python
201
+ from ui_mcp.cli_runner import run_cli
202
+ result = run_cli(["lo", "health"])
203
+ print(result)
204
+ ```
205
+
206
+ ## Troubleshooting
207
+
208
+ ### "Conda environment not found"
209
+
210
+ The wrapper script couldn't find the `ui-cli` conda environment. Ensure:
211
+
212
+ ```bash
213
+ conda activate ui-cli
214
+ ./ui mcp install # Re-install with correct Python path
215
+ ```
216
+
217
+ ### "No output" in Claude Desktop
218
+
219
+ 1. Restart Claude Desktop after config changes
220
+ 2. Check that `.env` file exists with credentials
221
+ 3. Run `./ui mcp check` to verify setup
222
+
223
+ ### Tools timeout
224
+
225
+ Increase timeout in `cli_runner.py` for slow operations:
226
+
227
+ ```python
228
+ result = run_cli(["lo", "health"], timeout=60)
229
+ ```
230
+
231
+ ## Version History
232
+
233
+ - **v0.3.0** - Tools layer architecture with quick timeout support
234
+ - **v0.2.0** - Tools layer architecture (subprocess-based)
235
+ - **v0.1.0** - Direct API calls (deprecated)
ui_mcp/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """UI-CLI MCP Server v2.
2
+
3
+ Model Context Protocol server for managing UniFi infrastructure
4
+ through Claude Desktop. Uses a tools layer that calls the UI CLI.
5
+ """
6
+
7
+ from ui_cli import __version__
ui_mcp/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Entry point for running the MCP server.
2
+
3
+ Usage:
4
+ python -m ui_mcp
5
+ """
6
+
7
+ from ui_mcp.server import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
ui_mcp/cli_runner.py ADDED
@@ -0,0 +1,112 @@
1
+ """CLI runner for MCP tools.
2
+
3
+ Executes UI CLI commands via subprocess and returns parsed JSON output.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # Project root is parent of src/
13
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
14
+
15
+
16
+ def run_cli(
17
+ args: list[str],
18
+ timeout: int = 30,
19
+ skip_confirmation: bool = True,
20
+ ) -> dict:
21
+ """Run UI CLI command and return parsed output.
22
+
23
+ Args:
24
+ args: Command arguments (e.g., ["lo", "health", "-o", "json"])
25
+ timeout: Command timeout in seconds
26
+ skip_confirmation: Add -y flag to skip confirmations for actions
27
+
28
+ Returns:
29
+ Parsed JSON output or error dict with 'error' key
30
+ """
31
+ # Use the same Python that's running the MCP server
32
+ # This ensures we're in the correct conda environment
33
+ python_path = sys.executable
34
+ cmd = [python_path, "-m", "ui_cli.main"] + args
35
+
36
+ # Add JSON output flag if not present
37
+ if "-o" not in args and "--output" not in args:
38
+ cmd.extend(["-o", "json"])
39
+
40
+ # Add -y for action commands to skip confirmation
41
+ if skip_confirmation and any(
42
+ action in args for action in ["block", "unblock", "kick", "restart", "create"]
43
+ ):
44
+ if "-y" not in args and "--yes" not in args:
45
+ cmd.append("-y")
46
+
47
+ # Set up environment with PYTHONPATH
48
+ env = os.environ.copy()
49
+ env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
50
+
51
+ try:
52
+ result = subprocess.run(
53
+ cmd,
54
+ cwd=PROJECT_ROOT,
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=timeout,
58
+ env=env,
59
+ )
60
+
61
+ # Try to parse stdout as JSON
62
+ stdout = result.stdout.strip()
63
+ if stdout:
64
+ try:
65
+ return json.loads(stdout)
66
+ except json.JSONDecodeError:
67
+ # Not JSON, return as raw output
68
+ if result.returncode == 0:
69
+ return {"output": stdout}
70
+ else:
71
+ return {
72
+ "error": True,
73
+ "message": result.stderr.strip() or stdout,
74
+ "exit_code": result.returncode,
75
+ }
76
+
77
+ # No stdout, check for errors
78
+ if result.returncode != 0:
79
+ return {
80
+ "error": True,
81
+ "message": result.stderr.strip() or "Command failed with no output",
82
+ "exit_code": result.returncode,
83
+ }
84
+
85
+ return {"output": ""}
86
+
87
+ except subprocess.TimeoutExpired:
88
+ return {"error": True, "message": f"Command timed out after {timeout}s"}
89
+ except FileNotFoundError:
90
+ return {"error": True, "message": "CLI not found. Run from project root."}
91
+ except Exception as e:
92
+ return {"error": True, "message": str(e)}
93
+
94
+
95
+ def format_result(data: dict | list, summary: str | None = None) -> str:
96
+ """Format result dict/list as JSON string for MCP response.
97
+
98
+ Args:
99
+ data: Result data from CLI (dict or list)
100
+ summary: Optional human-readable summary to prepend
101
+
102
+ Returns:
103
+ JSON string suitable for MCP tool response
104
+ """
105
+ if isinstance(data, list):
106
+ # Wrap list in dict with summary
107
+ output = {"summary": summary, "data": data, "count": len(data)} if summary else {"data": data, "count": len(data)}
108
+ elif summary and "error" not in data:
109
+ output = {"summary": summary, **data}
110
+ else:
111
+ output = data
112
+ return json.dumps(output, indent=2)