diff --git a/atxpi/requirements.txt b/atxpi/requirements.txt index bd8a74c..7550d8e 100644 --- a/atxpi/requirements.txt +++ b/atxpi/requirements.txt @@ -1,6 +1,5 @@ # atxpi — зависимости (ставятся в venv на устройстве, armv7 / Python 3.13). # Шаг ① (core+CLI+mock) работает на чистом stdlib — ставить ничего не нужно. -# Ниже — полный набор для web-ui (REST + WS-консоль), кликабельного MCP и консоли. # RS232-консоль (real-драйвер) pyserial>=3.5 @@ -10,5 +9,7 @@ fastapi>=0.110 uvicorn>=0.29 websockets>=12 -# MCP-сервер для агентов (streamable-HTTP / stdio) -mcp>=1.2 +# MCP-сервер для агентов (streamable-http). Пин v1: FastMCP поддерживает +# встраивание/запуск через run(); mcp 2.x (MCPServer) требует собственного run() +# и не встраивается в FastAPI. Для отдельного процесса берём v1. +mcp>=1.2,<2 diff --git a/atxpi/webui/server.py b/atxpi/webui/server.py index fe0387a..49f710c 100644 --- a/atxpi/webui/server.py +++ b/atxpi/webui/server.py @@ -8,10 +8,12 @@ /api/status сводка по устройству /api/history журнал действий (audit) /ws/console WebSocket — двунаправленная RS232-консоль - /mcp MCP-эндпоинт (streamable-http) для агентов Аутентификация: Bearer-токен (ATXPI_WEB_TOKEN) в заголовке Authorization (для /ws/console также принимается ?token=...). + +MCP-эндпоинт (/mcp) реализуется отдельным процессом (см. atxpi/mcp_server.py), +т.к. mcp-SDK требует собственного run() для streamable-http транспорта. """ import asyncio import json @@ -27,8 +29,6 @@ from fastapi.staticfiles import StaticFiles from atxpi.core import power as power_mod from atxpi.core.hardware import make_console, DRIVER, SERIAL_PORT, SERIAL_BAUD -from mcp.server.mcpserver import MCPServer - log = logging.getLogger("atxpi.webui") logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") @@ -65,9 +65,9 @@ app = FastAPI(title="atxpi", version="0.1.0") @app.middleware("http") async def auth_mw(request: Request, call_next): - """Требуем Bearer-токен для /api/* и /mcp/* (если токен задан).""" + """Требуем Bearer-токен для /api/* (если токен задан).""" path = request.url.path - if path.startswith(("/api", "/mcp")) and WEB_TOKEN: + if path.startswith("/api") and WEB_TOKEN: got = request.headers.get("authorization", "").replace("Bearer ", "").strip() if got != WEB_TOKEN: return JSONResponse({"detail": "unauthorized"}, status_code=401) @@ -170,48 +170,6 @@ async def ws_console(ws: WebSocket): console.close() -# ── MCP (streamable-http endpoint, для агентов) ───────────────────────────── -mcp = MCPServer("atxpi") - - -@mcp.tool() -def power_status() -> dict: - """Текущее состояние питания сервера (on/off).""" - return power_mod.status() - - -@mcp.tool() -def power_on() -> dict: - """Нажать кнопку питания (вкл/выкл-переключатель).""" - audit("power_on") - return power_mod.press_power() - - -@mcp.tool() -def power_off() -> dict: - """Принудительное выключение (длинное нажатие кнопки питания).""" - audit("power_off") - return power_mod.force_off() - - -@mcp.tool() -def power_reset() -> dict: - """Аппаратный сброс сервера.""" - audit("power_reset") - return power_mod.press_reset() - - -@mcp.tool() -def power_cycle() -> dict: - """Жёсткий перезапуск: выключить, пауза, включить.""" - audit("power_cycle") - return power_mod.cycle() - - -# Суб-приложение mcp уже содержит маршрут `/mcp` → монтируем его в КОРЕНЬ -# (см. app.mount("/", ...) в самом низу файла), чтобы наш /mcp отдавал его роут. - - # ── статика ───────────────────────────────────────────────────────────────── @app.get("/") async def index(): @@ -219,8 +177,3 @@ async def index(): app.mount("/static", StaticFiles(directory=STATIC), name="static") - -# MCP-эндпоинт. streamable_http_app() имеет собственный маршрут `/mcp`, поэтому -# монтируем суб-приложение в корень ПОСЛЕ всех наших маршрутов — тогда -# /api/*, /, /static обрабатываются первыми, а /mcp отдаёт MCP-протокол. -app.mount("/", mcp.streamable_http_app(), name="mcp")