Fix UniFi OS authentication and simplify server architecture
All checks were successful
Build and Push Docker Image / build (push) Successful in 15s
All checks were successful
Build and Push Docker Image / build (push) Successful in 15s
- Use /api/auth/login for UniFi OS controllers (UDM, Cloud Gateway) - Use /api/login for standalone controllers - Refactor to use FastMCP's native mcp.run() with custom_route for /health - Switch to network_mode: host for local network access - Include README.md in Dockerfile for hatchling build
This commit is contained in:
@@ -6,8 +6,8 @@ WORKDIR /app
|
|||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN pip install --no-cache-dir uv
|
RUN pip install --no-cache-dir uv
|
||||||
|
|
||||||
# Copy dependency files
|
# Copy dependency files and README (required by hatchling)
|
||||||
COPY pyproject.toml ./
|
COPY pyproject.toml README.md ./
|
||||||
|
|
||||||
# Create virtual environment and install dependencies
|
# Create virtual environment and install dependencies
|
||||||
RUN uv venv /app/.venv
|
RUN uv venv /app/.venv
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ services:
|
|||||||
image: gitea.ext.ben.io/b3nw/unifi-mcp-light:latest
|
image: gitea.ext.ben.io/b3nw/unifi-mcp-light:latest
|
||||||
container_name: unifi-mcp-light
|
container_name: unifi-mcp-light
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
network_mode: host
|
||||||
- "${PORT:-8100}:8000"
|
|
||||||
environment:
|
environment:
|
||||||
- UNIFI_HOST=${UNIFI_HOST}
|
- UNIFI_HOST=${UNIFI_HOST}
|
||||||
- UNIFI_USERNAME=${UNIFI_USERNAME}
|
- UNIFI_USERNAME=${UNIFI_USERNAME}
|
||||||
|
|||||||
45
server.py
45
server.py
@@ -11,18 +11,14 @@ Following the BLUEPRINT.md pattern:
|
|||||||
- Starlette wrapper with health endpoint
|
- Starlette wrapper with health endpoint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from typing import Optional
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.routing import Mount, Route
|
|
||||||
|
|
||||||
from api_docs import get_api_docs
|
from api_docs import get_api_docs
|
||||||
from unifi_client import UnifiClient, UnifiClientError, UnifiWriteBlockedError
|
from unifi_client import UnifiClient, UnifiClientError, UnifiWriteBlockedError
|
||||||
@@ -355,11 +351,14 @@ async def unifi_api_call(
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Starlette Wrapper with Health Endpoint
|
# Health Check Endpoint (using FastMCP custom_route)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
async def health_endpoint(request) -> JSONResponse:
|
|
||||||
|
@mcp.custom_route("/health", methods=["GET"])
|
||||||
|
async def health_endpoint(request: Request) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
Health check endpoint for Docker/Kubernetes.
|
Health check endpoint for Docker/Kubernetes.
|
||||||
|
|
||||||
@@ -386,9 +385,8 @@ async def health_endpoint(request) -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
def main():
|
||||||
async def lifespan(app):
|
"""Entry point for the server."""
|
||||||
"""Application lifespan manager."""
|
|
||||||
logger.info("Starting UniFi MCP Light server...")
|
logger.info("Starting UniFi MCP Light server...")
|
||||||
logger.info(f"Controller: {CONFIG['host']}:{CONFIG['port']}")
|
logger.info(f"Controller: {CONFIG['host']}:{CONFIG['port']}")
|
||||||
logger.info(f"Site: {CONFIG['site']}")
|
logger.info(f"Site: {CONFIG['site']}")
|
||||||
@@ -396,31 +394,8 @@ async def lifespan(app):
|
|||||||
f"Write operations: {'enabled' if CONFIG['allow_writes'] else 'disabled'}"
|
f"Write operations: {'enabled' if CONFIG['allow_writes'] else 'disabled'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
yield
|
# Run with HTTP transport - MCP endpoint at /mcp, health at /health
|
||||||
|
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -158,8 +158,17 @@ class UnifiClient:
|
|||||||
logger.warning("Controller type detection failed, defaulting to UniFi OS")
|
logger.warning("Controller type detection failed, defaulting to UniFi OS")
|
||||||
|
|
||||||
async def _authenticate(self) -> None:
|
async def _authenticate(self) -> None:
|
||||||
"""Authenticate with the UniFi controller."""
|
"""Authenticate with the UniFi controller.
|
||||||
login_url = f"{self.base_url}{self.api_prefix}/api/login"
|
|
||||||
|
UniFi OS (UDM, Cloud Gateway) uses /api/auth/login
|
||||||
|
Standalone controllers use /api/login
|
||||||
|
"""
|
||||||
|
if self._is_unifi_os:
|
||||||
|
# UniFi OS uses a different auth endpoint (no api_prefix needed)
|
||||||
|
login_url = f"{self.base_url}/api/auth/login"
|
||||||
|
else:
|
||||||
|
# Standalone controller
|
||||||
|
login_url = f"{self.base_url}/api/login"
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user