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,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,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
ui_mcp/__main__.py
ADDED
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)
|