Initial implementation of UniFi MCP Light server
Some checks failed
Build and Push Docker Image / build (push) Failing after 12s
Some checks failed
Build and Push Docker Image / build (push) Failing after 12s
Implements Hybrid MCP Light pattern with: - 4 specific tools: list_clients, list_devices, get_system_info, get_network_health - Meta tools: tool_index, api_call (raw API pass-through) - API documentation resource at unifi://api-reference - Starlette wrapper with /health endpoint - Write protection (UNIFI_ALLOW_WRITES env var) - UniFi OS auto-detection (proxy vs direct paths) - Docker multi-stage build - Gitea CI workflow Closes #1, #2, #3, #4, #5, #7
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# UniFi Controller Configuration
|
||||||
|
UNIFI_HOST=192.168.1.1
|
||||||
|
UNIFI_USERNAME=admin
|
||||||
|
UNIFI_PASSWORD=your_password_here
|
||||||
|
UNIFI_PORT=443
|
||||||
|
UNIFI_SITE=default
|
||||||
|
UNIFI_VERIFY_SSL=false
|
||||||
|
|
||||||
|
# Security Settings
|
||||||
|
# Enable write operations (POST, PUT, DELETE, PATCH) via unifi_api_call
|
||||||
|
# Default: false (read-only mode)
|
||||||
|
UNIFI_ALLOW_WRITES=false
|
||||||
30
.gitea/workflows/build.yaml
Normal file
30
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.ext.ben.io
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.CR_PAT }}
|
||||||
|
|
||||||
|
- name: Build and Push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: gitea.ext.ben.io/${{ gitea.repository }}:latest
|
||||||
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
1
BLUEPRINT.md
Symbolic link
1
BLUEPRINT.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../BLUEPRINT.md
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN pip install --no-cache-dir uv
|
||||||
|
|
||||||
|
# Copy dependency files
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
|
||||||
|
# Create virtual environment and install dependencies
|
||||||
|
RUN uv venv /app/.venv
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
RUN uv pip install --no-cache .
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy virtual environment from builder
|
||||||
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY server.py unifi_client.py api_docs.py ./
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd --create-home --shell /bin/bash appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "server.py"]
|
||||||
215
IMPLEMENTATION.md
Normal file
215
IMPLEMENTATION.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Implementation Details
|
||||||
|
|
||||||
|
Technical documentation for the UniFi MCP Light server architecture.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Starlette ASGI App │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ /health │ / (MCP via FastMCP) │
|
||||||
|
│ Health Check │ ┌────────────────────────────┐ │
|
||||||
|
│ │ │ MCP Protocol │ │
|
||||||
|
│ │ │ - Tools (6) │ │
|
||||||
|
│ │ │ - Resources (1) │ │
|
||||||
|
│ │ └────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ UnifiClient │
|
||||||
|
│ - Session management │
|
||||||
|
│ - UniFi OS detection │
|
||||||
|
│ - Write protection │
|
||||||
|
│ - Cookie-based auth │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ aiohttp │
|
||||||
|
│ - HTTPS connections │
|
||||||
|
│ - SSL handling │
|
||||||
|
│ - Cookie jar │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
unifi-mcp-light/
|
||||||
|
├── server.py # MCP server + Starlette wrapper + tools
|
||||||
|
├── unifi_client.py # UniFi API client abstraction
|
||||||
|
├── api_docs.py # Embedded API documentation
|
||||||
|
├── pyproject.toml # Python package configuration
|
||||||
|
├── Dockerfile # Multi-stage container build
|
||||||
|
├── docker-compose.yml # Container orchestration
|
||||||
|
└── .gitea/workflows/
|
||||||
|
└── build.yaml # CI pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
## UniFi Client Design
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
The `UnifiClient` class manages a single `aiohttp.ClientSession` with:
|
||||||
|
- Cookie jar for session persistence
|
||||||
|
- SSL context (verification optional)
|
||||||
|
- CSRF token extraction (UniFi OS)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with UnifiClient(host, username, password) as client:
|
||||||
|
clients = await client.get_clients()
|
||||||
|
```
|
||||||
|
|
||||||
|
### UniFi OS Detection
|
||||||
|
|
||||||
|
Modern UniFi OS devices (Cloud Gateway, UDM-Pro, etc.) use a proxy path structure:
|
||||||
|
|
||||||
|
| Controller Type | API Path |
|
||||||
|
|-----------------|----------|
|
||||||
|
| UniFi OS | `/proxy/network/api/s/{site}/...` |
|
||||||
|
| Standalone | `/api/s/{site}/...` |
|
||||||
|
|
||||||
|
Detection probes both paths during initialization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _detect_controller_type(self):
|
||||||
|
# Try UniFi OS path first (more common)
|
||||||
|
try:
|
||||||
|
resp = await session.get(f"{base}/proxy/network/api/s/{site}/stat/health")
|
||||||
|
if resp.status in (200, 401, 403):
|
||||||
|
self._is_unifi_os = True
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fall back to direct path
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Write Protection
|
||||||
|
|
||||||
|
Write operations require explicit opt-in:
|
||||||
|
|
||||||
|
```python
|
||||||
|
WRITE_METHODS = frozenset({'POST', 'PUT', 'DELETE', 'PATCH'})
|
||||||
|
|
||||||
|
async def request(self, method, endpoint, payload=None):
|
||||||
|
if method.upper() in WRITE_METHODS and not self.allow_writes:
|
||||||
|
raise UnifiWriteBlockedError(
|
||||||
|
"Write operation blocked. Set UNIFI_ALLOW_WRITES=true"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Pass-through Pattern
|
||||||
|
|
||||||
|
The `unifi_api_call` tool provides the "escape hatch":
|
||||||
|
|
||||||
|
1. Accepts any endpoint path
|
||||||
|
2. Automatically prepends UniFi OS prefix if needed
|
||||||
|
3. Parses JSON params for request body
|
||||||
|
4. Enforces write protection
|
||||||
|
5. Returns raw API response
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_api_call(endpoint: str, method: str = "GET", params: str = "{}"):
|
||||||
|
payload = json.loads(params) if params != "{}" else None
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.request(method, endpoint, payload)
|
||||||
|
return json.dumps(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All errors are returned as JSON objects:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Error message here",
|
||||||
|
"hint": "Optional suggestion for resolution"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exception hierarchy:
|
||||||
|
- `UnifiClientError`: Base class for all client errors
|
||||||
|
- `UnifiAuthError`: Authentication failures
|
||||||
|
- `UnifiWriteBlockedError`: Write operation attempted without permission
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
The `/health` endpoint verifies:
|
||||||
|
|
||||||
|
1. Client is connected
|
||||||
|
2. Controller is reachable
|
||||||
|
3. Authentication is valid
|
||||||
|
|
||||||
|
Response format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"controller": "192.168.1.1",
|
||||||
|
"site": "default",
|
||||||
|
"write_enabled": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Error response (503):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "Connection refused"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Resources
|
||||||
|
|
||||||
|
The `unifi://api-reference` resource provides:
|
||||||
|
- Endpoint documentation
|
||||||
|
- Request/response examples
|
||||||
|
- Query parameter options
|
||||||
|
- Write operation requirements
|
||||||
|
|
||||||
|
This enables AI agents to discover API capabilities without external documentation.
|
||||||
|
|
||||||
|
## Docker Build
|
||||||
|
|
||||||
|
Multi-stage build for minimal image size:
|
||||||
|
|
||||||
|
1. **Builder stage**: Install dependencies with UV
|
||||||
|
2. **Runtime stage**: Copy venv, run as non-root user
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
# Install dependencies
|
||||||
|
RUN uv venv && uv pip install .
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
# Copy venv, set non-root user
|
||||||
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
|
USER appuser
|
||||||
|
CMD ["python", "server.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending
|
||||||
|
|
||||||
|
### Adding a New Tool
|
||||||
|
|
||||||
|
1. Add tool function to `server.py`:
|
||||||
|
```python
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_new_tool() -> str:
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.request("GET", f"/api/s/{client.site}/...")
|
||||||
|
return json.dumps(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add to `unifi_tool_index` catalog
|
||||||
|
|
||||||
|
3. Consider if it should count toward the 4-tool limit
|
||||||
|
|
||||||
|
### Adding Client Methods
|
||||||
|
|
||||||
|
Add convenience methods to `UnifiClient`:
|
||||||
|
```python
|
||||||
|
async def get_events(self, limit: int = 100) -> list:
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
f"/api/s/{self.site}/stat/event?_limit={limit}"
|
||||||
|
)
|
||||||
|
```
|
||||||
158
README.md
Normal file
158
README.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# UniFi MCP Light
|
||||||
|
|
||||||
|
A **Hybrid MCP Light Server** for UniFi Network Controller following the minimal-surface-area pattern with raw API pass-through.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **4 Specific Tools**: Common read operations for clients, devices, system info, and health
|
||||||
|
- **Raw API Pass-through**: Access any UniFi API endpoint via `unifi_api_call`
|
||||||
|
- **Embedded Documentation**: API reference available as MCP Resource
|
||||||
|
- **Write Protection**: Mutations disabled by default, enable with `UNIFI_ALLOW_WRITES=true`
|
||||||
|
- **UniFi OS Support**: Auto-detects Cloud Gateway, UDM, and standalone controllers
|
||||||
|
- **Health Endpoint**: `/health` for Docker health checks
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Docker (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 8100:8000 \
|
||||||
|
-e UNIFI_HOST=192.168.1.1 \
|
||||||
|
-e UNIFI_USERNAME=admin \
|
||||||
|
-e UNIFI_PASSWORD=your_password \
|
||||||
|
gitea.ext.ben.io/b3nw/unifi-mcp-light:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your UniFi credentials
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python / UV
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install UV
|
||||||
|
curl -fsSL https://astral.sh/uv/install.sh | bash
|
||||||
|
|
||||||
|
# Clone and install
|
||||||
|
git clone gitea@git.local.ben.io:b3nw/unifi-mcp-light.git
|
||||||
|
cd unifi-mcp-light
|
||||||
|
uv venv && source .venv/bin/activate
|
||||||
|
uv pip install -e .
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your credentials
|
||||||
|
|
||||||
|
# Run
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `UNIFI_HOST` | Controller IP/hostname | **required** |
|
||||||
|
| `UNIFI_USERNAME` | Admin username | **required** |
|
||||||
|
| `UNIFI_PASSWORD` | Admin password | **required** |
|
||||||
|
| `UNIFI_PORT` | HTTPS port | `443` |
|
||||||
|
| `UNIFI_SITE` | Site name | `default` |
|
||||||
|
| `UNIFI_VERIFY_SSL` | Verify SSL certificate | `false` |
|
||||||
|
| `UNIFI_ALLOW_WRITES` | Enable write operations | `false` |
|
||||||
|
| `PORT` | Host port (docker-compose) | `8100` |
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### Specific Tools (4)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `unifi_list_clients` | List connected network clients (phones, laptops, IoT) |
|
||||||
|
| `unifi_list_devices` | List UniFi devices (APs, switches, gateways) |
|
||||||
|
| `unifi_get_system_info` | Get controller version and configuration |
|
||||||
|
| `unifi_get_network_health` | Get health status per subsystem (WAN, LAN, WLAN) |
|
||||||
|
|
||||||
|
### Meta Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `unifi_tool_index` | Get catalog of all available tools with schemas |
|
||||||
|
| `unifi_api_call` | Raw API pass-through (the "escape hatch") |
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
| URI | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `unifi://api-reference` | Curated UniFi API documentation |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### List Connected Clients
|
||||||
|
|
||||||
|
```
|
||||||
|
Use the unifi_list_clients tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw API Call (Read)
|
||||||
|
|
||||||
|
```
|
||||||
|
Use unifi_api_call with:
|
||||||
|
- endpoint: /api/s/default/stat/event
|
||||||
|
- method: GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw API Call (Write - requires UNIFI_ALLOW_WRITES=true)
|
||||||
|
|
||||||
|
```
|
||||||
|
Use unifi_api_call with:
|
||||||
|
- endpoint: /api/s/default/cmd/stamgr
|
||||||
|
- method: POST
|
||||||
|
- params: {"cmd": "block-sta", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Claude Desktop Integration
|
||||||
|
|
||||||
|
Add to your `claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"unifi": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run", "--rm", "-i",
|
||||||
|
"-e", "UNIFI_HOST=192.168.1.1",
|
||||||
|
"-e", "UNIFI_USERNAME=admin",
|
||||||
|
"-e", "UNIFI_PASSWORD=your_password",
|
||||||
|
"gitea.ext.ben.io/b3nw/unifi-mcp-light:latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Write operations are disabled by default**
|
||||||
|
- Set `UNIFI_ALLOW_WRITES=true` only when needed
|
||||||
|
- Use read-only credentials when possible
|
||||||
|
- Don't expose the server to the internet without authentication
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This server follows the **Hybrid MCP Light** pattern:
|
||||||
|
|
||||||
|
1. **Minimal Surface Area**: Only 4 specific tools for common operations
|
||||||
|
2. **Escape Hatch**: Raw API pass-through for any other operation
|
||||||
|
3. **Embedded Documentation**: API docs as MCP Resource
|
||||||
|
4. **Starlette Wrapper**: Health checks for container orchestration
|
||||||
|
|
||||||
|
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for technical details.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
357
api_docs.py
Normal file
357
api_docs.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
"""
|
||||||
|
UniFi API Documentation Resource
|
||||||
|
|
||||||
|
Curated API reference for the UniFi Network Controller.
|
||||||
|
Exposed as an MCP Resource to help AI agents use the raw API pass-through tool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
API_DOCS = """
|
||||||
|
# UniFi Network Controller API Reference
|
||||||
|
|
||||||
|
This document provides a curated reference for the UniFi Network Controller API.
|
||||||
|
Use this with the `unifi_api_call` tool to execute raw API requests.
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- All endpoints use the site variable `{site}` which defaults to `default`
|
||||||
|
- The server automatically handles UniFi OS path prefixing (`/proxy/network`)
|
||||||
|
- Authentication is handled automatically via session cookies
|
||||||
|
- Write operations (POST, PUT, DELETE) require `UNIFI_ALLOW_WRITES=true`
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
All UniFi API responses follow this structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meta": {"rc": "ok"},
|
||||||
|
"data": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `unifi_api_call` tool returns only the `data` array.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clients (Stations)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/sta
|
||||||
|
List all currently connected clients.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `mac`: Client MAC address
|
||||||
|
- `ip`: IP address
|
||||||
|
- `hostname`: Client hostname
|
||||||
|
- `name`: Friendly name (if set)
|
||||||
|
- `oui`: Manufacturer OUI
|
||||||
|
- `is_wired`: Boolean, true if wired connection
|
||||||
|
- `is_guest`: Boolean, true if on guest network
|
||||||
|
- `rx_bytes`, `tx_bytes`: Traffic counters
|
||||||
|
- `uptime`: Connection uptime in seconds
|
||||||
|
- `last_seen`: Unix timestamp of last activity
|
||||||
|
- `essid`: Connected SSID (wireless only)
|
||||||
|
- `channel`: WiFi channel (wireless only)
|
||||||
|
- `signal`: Signal strength in dBm (wireless only)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/alluser
|
||||||
|
List all known clients (including offline/historical).
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/user/{mac}
|
||||||
|
Get details for a specific client by MAC address.
|
||||||
|
|
||||||
|
### POST /api/s/{site}/cmd/stamgr
|
||||||
|
Client management commands. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
**Block a client:**
|
||||||
|
```json
|
||||||
|
{"cmd": "block-sta", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unblock a client:**
|
||||||
|
```json
|
||||||
|
{"cmd": "unblock-sta", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Force reconnect:**
|
||||||
|
```json
|
||||||
|
{"cmd": "kick-sta", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Devices
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/device
|
||||||
|
List all UniFi network devices (APs, switches, gateways).
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `mac`: Device MAC address
|
||||||
|
- `ip`: Management IP address
|
||||||
|
- `name`: Device name
|
||||||
|
- `model`: Model identifier (e.g., "U7PG2")
|
||||||
|
- `model_in_lts`: Long-term support model name
|
||||||
|
- `type`: Device type (uap, usw, ugw, udm)
|
||||||
|
- `version`: Firmware version
|
||||||
|
- `adopted`: Boolean, true if adopted
|
||||||
|
- `state`: Device state (1=connected, 0=disconnected)
|
||||||
|
- `uptime`: Uptime in seconds
|
||||||
|
- `num_sta`: Number of connected clients (APs)
|
||||||
|
- `port_table`: Port status array (switches)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/device/{mac}
|
||||||
|
Get details for a specific device by MAC address.
|
||||||
|
|
||||||
|
### POST /api/s/{site}/cmd/devmgr
|
||||||
|
Device management commands. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
**Restart device:**
|
||||||
|
```json
|
||||||
|
{"cmd": "restart", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adopt device:**
|
||||||
|
```json
|
||||||
|
{"cmd": "adopt", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Upgrade firmware:**
|
||||||
|
```json
|
||||||
|
{"cmd": "upgrade", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Locate device (flash LED):**
|
||||||
|
```json
|
||||||
|
{"cmd": "set-locate", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stop locating:**
|
||||||
|
```json
|
||||||
|
{"cmd": "unset-locate", "mac": "aa:bb:cc:dd:ee:ff"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Networks & VLANs
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/networkconf
|
||||||
|
List all network configurations.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `_id`: Network ID
|
||||||
|
- `name`: Network name
|
||||||
|
- `purpose`: Network purpose (corporate, guest, vlan-only)
|
||||||
|
- `vlan`: VLAN ID
|
||||||
|
- `subnet`: Network subnet (CIDR)
|
||||||
|
- `dhcpd_enabled`: DHCP server enabled
|
||||||
|
- `dhcpd_start`, `dhcpd_stop`: DHCP range
|
||||||
|
- `domain_name`: DHCP domain name
|
||||||
|
- `igmp_snooping`: IGMP snooping enabled
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/networkconf/{id}
|
||||||
|
Get specific network by ID.
|
||||||
|
|
||||||
|
### POST /api/s/{site}/rest/networkconf
|
||||||
|
Create new network. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
### PUT /api/s/{site}/rest/networkconf/{id}
|
||||||
|
Update network. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
### DELETE /api/s/{site}/rest/networkconf/{id}
|
||||||
|
Delete network. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wireless Networks (WLANs)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/wlanconf
|
||||||
|
List all wireless network configurations.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `_id`: WLAN ID
|
||||||
|
- `name`: SSID name
|
||||||
|
- `enabled`: Boolean
|
||||||
|
- `security`: Security type (wpapsk, open)
|
||||||
|
- `wpa_mode`: WPA mode (wpa2, wpa3)
|
||||||
|
- `x_passphrase`: WiFi password
|
||||||
|
- `hide_ssid`: Hidden network
|
||||||
|
- `is_guest`: Guest network
|
||||||
|
- `vlan`: VLAN ID
|
||||||
|
- `schedule`: Scheduling configuration
|
||||||
|
|
||||||
|
### POST /api/s/{site}/rest/wlanconf
|
||||||
|
Create new WLAN. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
### PUT /api/s/{site}/rest/wlanconf/{id}
|
||||||
|
Update WLAN. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/firewallrule
|
||||||
|
List firewall rules (legacy format).
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/firewallgroup
|
||||||
|
List firewall groups (IP groups, port groups).
|
||||||
|
|
||||||
|
**Response fields (group):**
|
||||||
|
- `_id`: Group ID
|
||||||
|
- `name`: Group name
|
||||||
|
- `group_type`: Type (address-group, port-group)
|
||||||
|
- `group_members`: Array of IPs or ports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Port Forwarding
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/portforward
|
||||||
|
List port forwarding rules.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `_id`: Rule ID
|
||||||
|
- `name`: Rule name
|
||||||
|
- `enabled`: Boolean
|
||||||
|
- `src`: Source restriction (any, limited)
|
||||||
|
- `dst_port`: Destination port(s)
|
||||||
|
- `fwd`: Forward to IP address
|
||||||
|
- `fwd_port`: Forward to port
|
||||||
|
- `proto`: Protocol (tcp, udp, tcp_udp)
|
||||||
|
|
||||||
|
### POST /api/s/{site}/rest/portforward
|
||||||
|
Create port forward. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/routing
|
||||||
|
List static routes.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `_id`: Route ID
|
||||||
|
- `name`: Route name
|
||||||
|
- `enabled`: Boolean
|
||||||
|
- `type`: Route type (static, interface)
|
||||||
|
- `network`: Destination network
|
||||||
|
- `gateway`: Next hop gateway
|
||||||
|
- `interface`: Interface name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics & Health
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/health
|
||||||
|
Get overall network health status.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `subsystem`: Subsystem name (wan, lan, wlan, vpn)
|
||||||
|
- `status`: Status (ok, warning, error)
|
||||||
|
- `num_user`: Connected user count
|
||||||
|
- `num_guest`: Connected guest count
|
||||||
|
- `tx_bytes-r`, `rx_bytes-r`: Transfer rates
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/sysinfo
|
||||||
|
Get system information.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `version`: Controller version
|
||||||
|
- `build`: Build number
|
||||||
|
- `hostname`: Controller hostname
|
||||||
|
- `name`: Site name
|
||||||
|
- `timezone`: Timezone
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/event
|
||||||
|
List recent events.
|
||||||
|
|
||||||
|
**Query parameters:**
|
||||||
|
- `within`: Time range in hours (default: 24)
|
||||||
|
- `_limit`: Maximum events to return
|
||||||
|
|
||||||
|
**Example:** `/api/s/default/stat/event?within=24&_limit=100`
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/alarm
|
||||||
|
List active alarms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VPN
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/vpnclient
|
||||||
|
List VPN client configurations.
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/vpnserver
|
||||||
|
List VPN server configurations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hotspot & Vouchers
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/voucher
|
||||||
|
List hotspot vouchers.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `code`: Voucher code
|
||||||
|
- `create_time`: Creation timestamp
|
||||||
|
- `duration`: Duration in minutes
|
||||||
|
- `quota`: Usage quota (0=unlimited)
|
||||||
|
- `used`: Times used
|
||||||
|
- `qos_overwrite`: QoS override enabled
|
||||||
|
|
||||||
|
### POST /api/s/{site}/cmd/hotspot
|
||||||
|
Hotspot commands. Requires `UNIFI_ALLOW_WRITES=true`.
|
||||||
|
|
||||||
|
**Create vouchers:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "create-voucher",
|
||||||
|
"n": 5,
|
||||||
|
"quota": 1,
|
||||||
|
"expire": 1440,
|
||||||
|
"note": "Event guests"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Revoke voucher:**
|
||||||
|
```json
|
||||||
|
{"cmd": "delete-voucher", "_id": "voucher_id_here"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Groups (Bandwidth Profiles)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/rest/usergroup
|
||||||
|
List user groups / bandwidth profiles.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `_id`: Group ID
|
||||||
|
- `name`: Group name
|
||||||
|
- `qos_rate_max_up`: Upload limit (kbps, -1=unlimited)
|
||||||
|
- `qos_rate_max_down`: Download limit (kbps, -1=unlimited)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DPI (Deep Packet Inspection)
|
||||||
|
|
||||||
|
### GET /api/s/{site}/stat/dpi
|
||||||
|
Get DPI statistics (application usage).
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
- `by_app`: Usage by application
|
||||||
|
- `by_cat`: Usage by category
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Query Parameters
|
||||||
|
|
||||||
|
Many list endpoints support:
|
||||||
|
- `_limit`: Maximum results
|
||||||
|
- `_start`: Offset for pagination
|
||||||
|
- `_sort`: Sort field (prefix with - for descending)
|
||||||
|
|
||||||
|
**Example:** `/api/s/default/stat/sta?_limit=50&_sort=-rx_bytes`
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_docs() -> str:
|
||||||
|
"""Return the curated UniFi API documentation."""
|
||||||
|
return API_DOCS
|
||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
unifi-mcp-light:
|
||||||
|
build: .
|
||||||
|
image: gitea.ext.ben.io/b3nw/unifi-mcp-light:latest
|
||||||
|
container_name: unifi-mcp-light
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8100}:8000"
|
||||||
|
environment:
|
||||||
|
- UNIFI_HOST=${UNIFI_HOST}
|
||||||
|
- UNIFI_USERNAME=${UNIFI_USERNAME}
|
||||||
|
- UNIFI_PASSWORD=${UNIFI_PASSWORD}
|
||||||
|
- UNIFI_PORT=${UNIFI_PORT:-443}
|
||||||
|
- UNIFI_SITE=${UNIFI_SITE:-default}
|
||||||
|
- UNIFI_VERIFY_SSL=${UNIFI_VERIFY_SSL:-false}
|
||||||
|
- UNIFI_ALLOW_WRITES=${UNIFI_ALLOW_WRITES:-false}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
50
pyproject.toml
Normal file
50
pyproject.toml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[project]
|
||||||
|
name = "unifi-mcp-light"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Hybrid MCP Light Server for UniFi Network Controller - minimal tools with raw API pass-through"
|
||||||
|
readme = "README.md"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{name = "b3nw", email = "admin@ben.io"}
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "unifi", "network", "api", "llm"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastmcp>=2.0.0",
|
||||||
|
"aiounifi>=80",
|
||||||
|
"aiohttp>=3.9.0",
|
||||||
|
"starlette>=0.40.0",
|
||||||
|
"uvicorn>=0.30.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-asyncio>=0.23.0",
|
||||||
|
"aioresponses>=0.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
unifi-mcp-light = "server:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["."]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
427
server.py
Normal file
427
server.py
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
"""
|
||||||
|
UniFi MCP Light Server
|
||||||
|
|
||||||
|
A Hybrid MCP Light server for UniFi Network Controller.
|
||||||
|
Provides minimal specific tools with a raw API pass-through escape hatch.
|
||||||
|
|
||||||
|
Following the BLUEPRINT.md pattern:
|
||||||
|
- 4 specific tools for common operations
|
||||||
|
- Meta tools (tool_index, api_call) for flexibility
|
||||||
|
- Embedded API documentation resource
|
||||||
|
- Starlette wrapper with health endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.routing import Mount, Route
|
||||||
|
|
||||||
|
from api_docs import get_api_docs
|
||||||
|
from unifi_client import UnifiClient, UnifiClientError, UnifiWriteBlockedError
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("unifi-mcp-light")
|
||||||
|
|
||||||
|
# Configuration from environment
|
||||||
|
CONFIG = {
|
||||||
|
"host": os.getenv("UNIFI_HOST", ""),
|
||||||
|
"username": os.getenv("UNIFI_USERNAME", ""),
|
||||||
|
"password": os.getenv("UNIFI_PASSWORD", ""),
|
||||||
|
"port": int(os.getenv("UNIFI_PORT", "443")),
|
||||||
|
"site": os.getenv("UNIFI_SITE", "default"),
|
||||||
|
"verify_ssl": os.getenv("UNIFI_VERIFY_SSL", "false").lower() == "true",
|
||||||
|
"allow_writes": os.getenv("UNIFI_ALLOW_WRITES", "false").lower() == "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global client instance
|
||||||
|
_client: Optional[UnifiClient] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_client() -> UnifiClient:
|
||||||
|
"""Get or create the UniFi client singleton."""
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
if not CONFIG["host"]:
|
||||||
|
raise UnifiClientError("UNIFI_HOST environment variable not set")
|
||||||
|
if not CONFIG["username"]:
|
||||||
|
raise UnifiClientError("UNIFI_USERNAME environment variable not set")
|
||||||
|
if not CONFIG["password"]:
|
||||||
|
raise UnifiClientError("UNIFI_PASSWORD environment variable not set")
|
||||||
|
|
||||||
|
_client = UnifiClient(
|
||||||
|
host=CONFIG["host"],
|
||||||
|
username=CONFIG["username"],
|
||||||
|
password=CONFIG["password"],
|
||||||
|
port=CONFIG["port"],
|
||||||
|
site=CONFIG["site"],
|
||||||
|
verify_ssl=CONFIG["verify_ssl"],
|
||||||
|
allow_writes=CONFIG["allow_writes"],
|
||||||
|
)
|
||||||
|
await _client.connect()
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
async def close_client() -> None:
|
||||||
|
"""Close the UniFi client connection."""
|
||||||
|
global _client
|
||||||
|
if _client:
|
||||||
|
await _client.close()
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
# Create MCP server
|
||||||
|
mcp = FastMCP(
|
||||||
|
name="unifi-mcp-light",
|
||||||
|
instructions="""
|
||||||
|
UniFi Network Controller MCP Server (Light Edition)
|
||||||
|
|
||||||
|
This server provides access to your UniFi Network Controller with:
|
||||||
|
- 4 specific tools for common read operations
|
||||||
|
- Raw API pass-through for any endpoint
|
||||||
|
- API documentation resource for reference
|
||||||
|
|
||||||
|
Use 'unifi://api-reference' resource for API documentation.
|
||||||
|
Write operations require UNIFI_ALLOW_WRITES=true.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MCP Resources
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("unifi://api-reference")
|
||||||
|
def api_reference() -> str:
|
||||||
|
"""
|
||||||
|
UniFi API Reference Documentation.
|
||||||
|
|
||||||
|
Returns curated documentation for UniFi Network Controller API endpoints.
|
||||||
|
Use this to understand how to construct raw API calls with unifi_api_call.
|
||||||
|
"""
|
||||||
|
return get_api_docs()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Specific Tools (4 total - don't exceed BLUEPRINT limit)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_list_clients() -> str:
|
||||||
|
"""
|
||||||
|
List all currently connected network clients.
|
||||||
|
|
||||||
|
Returns information about all devices (phones, laptops, IoT devices, etc.)
|
||||||
|
currently connected to your UniFi network, including:
|
||||||
|
- MAC and IP addresses
|
||||||
|
- Hostname and friendly name
|
||||||
|
- Connection type (wired/wireless)
|
||||||
|
- Traffic statistics
|
||||||
|
- Signal strength (wireless clients)
|
||||||
|
- Connected SSID (wireless clients)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON array of client objects
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.get_clients()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
except UnifiClientError as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_list_devices() -> str:
|
||||||
|
"""
|
||||||
|
List all UniFi network devices.
|
||||||
|
|
||||||
|
Returns information about all adopted UniFi devices including:
|
||||||
|
- Access Points (UAP)
|
||||||
|
- Switches (USW)
|
||||||
|
- Gateways/Routers (UGW, UDM)
|
||||||
|
- Other UniFi devices
|
||||||
|
|
||||||
|
Each device includes:
|
||||||
|
- Model and firmware version
|
||||||
|
- IP address and status
|
||||||
|
- Uptime and connected clients
|
||||||
|
- Port information (switches)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON array of device objects
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.get_devices()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
except UnifiClientError as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_get_system_info() -> str:
|
||||||
|
"""
|
||||||
|
Get UniFi controller system information.
|
||||||
|
|
||||||
|
Returns controller details including:
|
||||||
|
- Controller version and build
|
||||||
|
- Hostname and site name
|
||||||
|
- Timezone configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON array with system info object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.get_system_info()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
except UnifiClientError as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_get_network_health() -> str:
|
||||||
|
"""
|
||||||
|
Get overall network health status.
|
||||||
|
|
||||||
|
Returns health metrics for each network subsystem:
|
||||||
|
- WAN: Internet connectivity status
|
||||||
|
- LAN: Local network status
|
||||||
|
- WLAN: Wireless network status
|
||||||
|
- VPN: VPN tunnel status
|
||||||
|
|
||||||
|
Each subsystem includes:
|
||||||
|
- Status (ok, warning, error)
|
||||||
|
- Connected user/guest counts
|
||||||
|
- Transfer rates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON array of health status objects per subsystem
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.get_health()
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
except UnifiClientError as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Meta Tools (don't count toward BLUEPRINT limit)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_tool_index() -> str:
|
||||||
|
"""
|
||||||
|
Get the complete index of available UniFi tools.
|
||||||
|
|
||||||
|
Returns a machine-readable catalog of all registered tools with their:
|
||||||
|
- Name and description
|
||||||
|
- Input parameter schemas
|
||||||
|
- Return type information
|
||||||
|
|
||||||
|
Use this for programmatic tool discovery and dynamic invocation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON object with tools array containing tool schemas
|
||||||
|
"""
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
# Get all registered tools from MCP
|
||||||
|
tool_definitions = [
|
||||||
|
{
|
||||||
|
"name": "unifi_list_clients",
|
||||||
|
"description": "List all currently connected network clients",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unifi_list_devices",
|
||||||
|
"description": "List all UniFi network devices (APs, switches, gateways)",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unifi_get_system_info",
|
||||||
|
"description": "Get UniFi controller system information",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unifi_get_network_health",
|
||||||
|
"description": "Get overall network health status",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unifi_api_call",
|
||||||
|
"description": "Execute raw UniFi API call (escape hatch)",
|
||||||
|
"parameters": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "API path (e.g., /api/s/default/stat/sta)",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "HTTP method (GET, POST, PUT, DELETE)",
|
||||||
|
"default": "GET",
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JSON string of request body/parameters",
|
||||||
|
"default": "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return json.dumps({"tools": tool_definitions}, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def unifi_api_call(
|
||||||
|
endpoint: str,
|
||||||
|
method: str = "GET",
|
||||||
|
params: str = "{}",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Execute a raw UniFi API call (escape hatch).
|
||||||
|
|
||||||
|
This tool provides direct access to any UniFi API endpoint.
|
||||||
|
Use the 'unifi://api-reference' resource for API documentation.
|
||||||
|
|
||||||
|
IMPORTANT: Write operations (POST, PUT, DELETE, PATCH) require
|
||||||
|
UNIFI_ALLOW_WRITES=true environment variable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint: API path (e.g., '/api/s/default/stat/sta')
|
||||||
|
The server automatically handles UniFi OS path prefixing.
|
||||||
|
method: HTTP method - GET, POST, PUT, DELETE, PATCH (default: GET)
|
||||||
|
params: JSON string of request body/parameters (default: "{}")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response from the UniFi API, or error object
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# List all events from last 24 hours
|
||||||
|
endpoint: "/api/s/default/stat/event"
|
||||||
|
method: "GET"
|
||||||
|
params: "{}"
|
||||||
|
|
||||||
|
# Get specific client by MAC
|
||||||
|
endpoint: "/api/s/default/stat/user/aa:bb:cc:dd:ee:ff"
|
||||||
|
method: "GET"
|
||||||
|
params: "{}"
|
||||||
|
|
||||||
|
# Block a client (requires UNIFI_ALLOW_WRITES=true)
|
||||||
|
endpoint: "/api/s/default/cmd/stamgr"
|
||||||
|
method: "POST"
|
||||||
|
params: '{"cmd": "block-sta", "mac": "aa:bb:cc:dd:ee:ff"}'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Parse params JSON
|
||||||
|
try:
|
||||||
|
payload = json.loads(params) if params and params != "{}" else None
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return json.dumps({"error": f"Invalid params JSON: {e}"})
|
||||||
|
|
||||||
|
client = await get_client()
|
||||||
|
result = await client.request(method, endpoint, payload)
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
|
except UnifiWriteBlockedError as e:
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"error": str(e),
|
||||||
|
"hint": "Set UNIFI_ALLOW_WRITES=true environment variable to enable write operations",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except UnifiClientError as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Starlette Wrapper with Health Endpoint
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def health_endpoint(request) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
Health check endpoint for Docker/Kubernetes.
|
||||||
|
|
||||||
|
Returns controller connectivity status and configuration.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_client()
|
||||||
|
await client.ping()
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"controller": CONFIG["host"],
|
||||||
|
"site": CONFIG["site"],
|
||||||
|
"write_enabled": CONFIG["allow_writes"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e),
|
||||||
|
},
|
||||||
|
status_code=503,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app):
|
||||||
|
"""Application lifespan manager."""
|
||||||
|
logger.info("Starting UniFi MCP Light server...")
|
||||||
|
logger.info(f"Controller: {CONFIG['host']}:{CONFIG['port']}")
|
||||||
|
logger.info(f"Site: {CONFIG['site']}")
|
||||||
|
logger.info(
|
||||||
|
f"Write operations: {'enabled' if CONFIG['allow_writes'] else 'disabled'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
logger.info("Shutting down UniFi MCP Light server...")
|
||||||
|
await close_client()
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> Starlette:
|
||||||
|
"""Create the Starlette ASGI application."""
|
||||||
|
mcp_app = mcp.http_app()
|
||||||
|
|
||||||
|
return Starlette(
|
||||||
|
routes=[
|
||||||
|
Route("/health", health_endpoint),
|
||||||
|
Mount("/", app=mcp_app),
|
||||||
|
],
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for the server."""
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
299
unifi_client.py
Normal file
299
unifi_client.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
"""
|
||||||
|
UniFi Controller Client
|
||||||
|
|
||||||
|
A clean abstraction layer wrapping aiohttp for UniFi API communication.
|
||||||
|
Handles authentication, UniFi OS detection, and write protection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import ssl
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# HTTP methods that modify state
|
||||||
|
WRITE_METHODS = frozenset({"POST", "PUT", "DELETE", "PATCH"})
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiClientError(Exception):
|
||||||
|
"""Base exception for UniFi client errors."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiAuthError(UnifiClientError):
|
||||||
|
"""Authentication failed."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiWriteBlockedError(UnifiClientError):
|
||||||
|
"""Write operation blocked by configuration."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiClient:
|
||||||
|
"""
|
||||||
|
Async client for UniFi Network Controller API.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Auto-detects UniFi OS vs standalone controller paths
|
||||||
|
- Cookie-based session authentication
|
||||||
|
- Write protection (configurable)
|
||||||
|
- Proper async context manager support
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
port: int = 443,
|
||||||
|
site: str = "default",
|
||||||
|
verify_ssl: bool = False,
|
||||||
|
allow_writes: bool = False,
|
||||||
|
):
|
||||||
|
self.host = host
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.port = port
|
||||||
|
self.site = site
|
||||||
|
self.verify_ssl = verify_ssl
|
||||||
|
self.allow_writes = allow_writes
|
||||||
|
|
||||||
|
self._session: Optional[aiohttp.ClientSession] = None
|
||||||
|
self._is_unifi_os: Optional[bool] = None
|
||||||
|
self._csrf_token: Optional[str] = None
|
||||||
|
self._authenticated = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return f"https://{self.host}:{self.port}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_prefix(self) -> str:
|
||||||
|
"""Return the correct API prefix based on controller type."""
|
||||||
|
if self._is_unifi_os:
|
||||||
|
return "/proxy/network"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "UnifiClient":
|
||||||
|
await self.connect()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def connect(self) -> None:
|
||||||
|
"""Initialize session, detect controller type, and authenticate."""
|
||||||
|
if self._session is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create SSL context
|
||||||
|
if self.verify_ssl:
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
else:
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||||
|
self._session = aiohttp.ClientSession(
|
||||||
|
connector=connector,
|
||||||
|
cookie_jar=aiohttp.CookieJar(unsafe=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Detect controller type and authenticate
|
||||||
|
await self._detect_controller_type()
|
||||||
|
await self._authenticate()
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the HTTP session."""
|
||||||
|
if self._session:
|
||||||
|
await self._session.close()
|
||||||
|
self._session = None
|
||||||
|
self._authenticated = False
|
||||||
|
|
||||||
|
async def _detect_controller_type(self) -> None:
|
||||||
|
"""
|
||||||
|
Detect if this is a UniFi OS controller (UDM, Cloud Gateway, etc.)
|
||||||
|
or a standalone Network Controller.
|
||||||
|
|
||||||
|
UniFi OS uses /proxy/network prefix for Network API calls.
|
||||||
|
"""
|
||||||
|
# Try UniFi OS path first (more common in modern setups)
|
||||||
|
try:
|
||||||
|
async with self._session.get(
|
||||||
|
f"{self.base_url}/proxy/network/api/s/{self.site}/stat/health",
|
||||||
|
timeout=aiohttp.ClientTimeout(total=5),
|
||||||
|
) as resp:
|
||||||
|
if resp.status in (200, 401, 403):
|
||||||
|
# Path exists, this is UniFi OS
|
||||||
|
self._is_unifi_os = True
|
||||||
|
logger.info("Detected UniFi OS controller (proxy path)")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try direct path (standalone controller)
|
||||||
|
try:
|
||||||
|
async with self._session.get(
|
||||||
|
f"{self.base_url}/api/s/{self.site}/stat/health",
|
||||||
|
timeout=aiohttp.ClientTimeout(total=5),
|
||||||
|
) as resp:
|
||||||
|
if resp.status in (200, 401, 403):
|
||||||
|
self._is_unifi_os = False
|
||||||
|
logger.info("Detected standalone UniFi controller (direct path)")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Default to UniFi OS if detection fails
|
||||||
|
self._is_unifi_os = True
|
||||||
|
logger.warning("Controller type detection failed, defaulting to UniFi OS")
|
||||||
|
|
||||||
|
async def _authenticate(self) -> None:
|
||||||
|
"""Authenticate with the UniFi controller."""
|
||||||
|
login_url = f"{self.base_url}{self.api_prefix}/api/login"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self._session.post(
|
||||||
|
login_url,
|
||||||
|
json=payload,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10),
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
self._authenticated = True
|
||||||
|
# Extract CSRF token if present (UniFi OS)
|
||||||
|
if "x-csrf-token" in resp.headers:
|
||||||
|
self._csrf_token = resp.headers["x-csrf-token"]
|
||||||
|
logger.info(f"Successfully authenticated to {self.host}")
|
||||||
|
elif resp.status == 401:
|
||||||
|
raise UnifiAuthError("Invalid username or password")
|
||||||
|
else:
|
||||||
|
text = await resp.text()
|
||||||
|
raise UnifiAuthError(
|
||||||
|
f"Authentication failed: {resp.status} - {text}"
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
raise UnifiClientError(f"Connection error during authentication: {e}")
|
||||||
|
|
||||||
|
async def ping(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the connection to the controller is alive.
|
||||||
|
Returns True if connected, raises exception otherwise.
|
||||||
|
"""
|
||||||
|
if not self._authenticated:
|
||||||
|
raise UnifiClientError("Not authenticated")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.request("GET", f"/api/s/{self.site}/stat/health")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
raise UnifiClientError(f"Controller ping failed: {e}")
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
endpoint: str,
|
||||||
|
payload: Optional[dict] = None,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Make an authenticated request to the UniFi API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
|
||||||
|
endpoint: API endpoint (e.g., /api/s/default/stat/sta)
|
||||||
|
payload: Optional request body for POST/PUT requests
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON response data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnifiWriteBlockedError: If write operation attempted without permission
|
||||||
|
UnifiClientError: For other API errors
|
||||||
|
"""
|
||||||
|
if not self._session:
|
||||||
|
raise UnifiClientError(
|
||||||
|
"Client not connected. Use 'async with' or call connect()"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check write protection
|
||||||
|
method_upper = method.upper()
|
||||||
|
if method_upper in WRITE_METHODS and not self.allow_writes:
|
||||||
|
raise UnifiWriteBlockedError(
|
||||||
|
f"Write operation ({method_upper}) blocked. "
|
||||||
|
"Set UNIFI_ALLOW_WRITES=true to enable write operations."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build full URL with correct prefix
|
||||||
|
url = f"{self.base_url}{self.api_prefix}{endpoint}"
|
||||||
|
|
||||||
|
# Build headers
|
||||||
|
headers = {}
|
||||||
|
if self._csrf_token:
|
||||||
|
headers["x-csrf-token"] = self._csrf_token
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self._session.request(
|
||||||
|
method_upper,
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=30),
|
||||||
|
) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
if resp.status == 401:
|
||||||
|
# Session expired, try to re-authenticate
|
||||||
|
await self._authenticate()
|
||||||
|
return await self.request(method, endpoint, payload)
|
||||||
|
|
||||||
|
if resp.status >= 400:
|
||||||
|
raise UnifiClientError(f"API error {resp.status}: {text}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"raw": text}
|
||||||
|
|
||||||
|
# UniFi API returns {"meta": {"rc": "ok"}, "data": [...]}
|
||||||
|
if isinstance(data, dict):
|
||||||
|
meta = data.get("meta", {})
|
||||||
|
if meta.get("rc") == "error":
|
||||||
|
raise UnifiClientError(
|
||||||
|
f"API error: {meta.get('msg', 'Unknown error')}"
|
||||||
|
)
|
||||||
|
if "data" in data:
|
||||||
|
return data["data"]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
raise UnifiClientError(f"Request failed: {e}")
|
||||||
|
|
||||||
|
async def get_clients(self) -> list:
|
||||||
|
"""Get all connected clients."""
|
||||||
|
return await self.request("GET", f"/api/s/{self.site}/stat/sta")
|
||||||
|
|
||||||
|
async def get_devices(self) -> list:
|
||||||
|
"""Get all network devices."""
|
||||||
|
return await self.request("GET", f"/api/s/{self.site}/stat/device")
|
||||||
|
|
||||||
|
async def get_system_info(self) -> list:
|
||||||
|
"""Get system information."""
|
||||||
|
return await self.request("GET", f"/api/s/{self.site}/stat/sysinfo")
|
||||||
|
|
||||||
|
async def get_health(self) -> list:
|
||||||
|
"""Get network health status."""
|
||||||
|
return await self.request("GET", f"/api/s/{self.site}/stat/health")
|
||||||
Reference in New Issue
Block a user