Files
obsidian-vault/personal/tech/balda-executor-mcp.md
T

320 lines
17 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# balda-executor-mcp — MCP-сервер делегирования задач (шаг 5 Фазы 4)
> Создан 2026-09-26. Шаг 5 плана [[personal/plans/balda-mm-phase4-agents]].
> Статус: **собран, задеплоен на Kraken, образ обновлён до 0.2.0; конфиг Балды
> переведён на `type: http`, Obsidian MCP прописан рядом** — ждёт сквозной проверки в чате.
> Связка: Балда (Kraken) → MCP → мост (Mac :8790) → контейнер-исполнитель.
---
## 1. Зачем отдельный MCP-сервер
Балда **не может** сама запустить CLI на Mac: Mac за NAT, а `runtime.mcp_servers` —
это либо stdio-процесс внутри контейнера Балды, либо HTTP по сети.
Схема выбрана такая:
```
Балда (Kraken, контейнер balda-mm)
│ runtime.mcp_servers.executor-mcp
▼
MCP-сервер balda-executor-mcp (Kraken, контейнер в mattermost_mm-net) ← HTTP
│ X-Bridge-Secret
▼
мост agents-bridge (Mac, 192.168.1.57:8790) ← обычный HTTP по LAN
│ docker exec
▼
контейнер balda-executor (Mac) → CLI claude/codex
```
---
## 2. Расположение и файлы
| Что | Путь |
|---|---|
| Проект | `~/Developer/balda-executor-mcp/` |
| Сервер | `server.py` (stdio + HTTP в одном файле) |
| Образ | `balda-executor-mcp:0.1.0` (python:3.12-slim-bookworm) |
| Dockerfile | `~/Developer/balda-executor-mcp/Dockerfile` |
| Compose (Kraken) | `/home/kraken/mcp/docker-compose.yml` |
| Контейнер на Kraken | `balda-executor-mcp` (сеть `mattermost_mm-net`) |
| Порядковый порт | `:8890` внутри docker-сети, **наружу не публикуется** |
---
## 3. Инструменты MCP
| Инструмент | Аргументы | Что делает |
|---|---|---|
| `executor_run` | `prompt` (обяз.), `provider?` (`claude`/`codex`), `idempotency_key?` | ставит задачу, возвращает `task_id` немедленно |
| `executor_status` | `task_id` | статус + stdout/stderr/exit_code |
| `executor_health` | — | живость моста и контейнера-исполнителя |
Ошибки возвращаются как `isError: true` с человеческим текстом:
- 429 → «Исполнитель занят (лимит одновременных задач)»
- 404 → «задача … не найдена»
- прочие → «мост вернул N: …»
---
## 4. Переменные окружения
| Ключ | Смысл |
|---|---|
| `BRIDGE_URL` | базовый URL моста (например `http://192.168.1.57:8790`) |
| `BRIDGE_SECRET` | общий секрет, уходит в `X-Bridge-Secret` |
| `BRIDGE_TIMEOUT` | таймаут обычных вызовов (по умолчанию 60 с) |
| `MCP_HOST` / `MCP_PORT` | адрес HTTP-режима (по умолчанию `127.0.0.1:8890`) |
---
## 5. Два режима запуска
```bash
# stdio (Балда сама запускает процесс)
python3 server.py
# HTTP (отдельный контейнер, Балда ходит по URL)
python3 server.py --http --host 0.0.0.0 --port 8890
```
⚠️ **Использовать надо HTTP-режим.** Причины — §7.
---
## 6. Приёмка (2026-09-26) — что именно проверено
| Проверка | Результат |
|---|---|
| stdio: `initialize` → `tools/list` | 3 инструмента возвращены ✅ |
| stdio: `tools/call` health | живые данные моста ✅ |
| stdio: 429 / 500 / 404 | корректные `isError` с текстом ✅ |
| образ: тот же handshake внутри контейнера | ✅ |
| образ на Kraken: загрузка | `Loaded image: balda-executor-mcp:0.1.0` ✅ |
| Kraken → мост по LAN (curl из docker-сети) | `status: ok` ✅ |
| MCP в docker-сети → `executor_health` через мост | живые данные Mac ✅ |
| контейнер `balda-executor-mcp` | Up ✅ |
Тестовые скрипты: `/tmp/test-mcp-server.sh` (мок моста + stdio-проба),
`/tmp/kraken-test-mcp.sh` (ручной handshake из docker-сети Kraken).
---
## 7. 🔴 Питфолл: stdio-вариант нерабочий в этом окружении
Изначальный план был `type: stdio` + `cmd: ["docker", "run", ...]` — то есть Балда
сама запускает MCP-контейнер. **Это не работает по двум причинам:**
1. **`docker.sock` в контейнер Балды не смонтирован.** Проверено:
`docker inspect balda-mm` даёт только три тома (config/state/workspace) — сокета нет.
Значит `docker run` изнутри Балды невозможен.
2. **В образе Балды нет shell.** `docker exec balda-mm sh -c ...` →
`OCI runtime exec failed: exec: "sh": executable file not found in $PATH`
(образ из `scratch`). Отладка и запуск подпроцессов через shell недоступны.
**Решение:** MCP-сервер вынести в **отдельный контейнер** с HTTP-транспортом.
Конфиг Балды тогда:
```yaml
runtime:
mcp_servers:
executor-mcp:
type: http
url: "http://balda-executor-mcp:8890/mcp"
balda:
mcp_servers:
- executor-mcp
```
---
## 8. 🔴 Питфолл: `--chmod=644` для файла, запускаемого не-root
Dockerfile изначально делал `COPY server.py /app/server.py` + `USER mcpsvc` → при
запуске: `python3: can't open file '/app/server.py': [Errno 13] Permission denied`.
Фикс: `COPY --chmod=644 server.py /app/server.py`. Не-root пользователь должен уметь
**читать** код, иначе контейнер падает ещё до handshake.
Симптом обманчив — выглядит как поломка образа, а не как права файла.
---
## 9. Питфолл: `docker-compose` на Mac, `docker compose` на Kraken
| Хост | Как вызывать |
|---|---|
| Mac (Colima) | **`docker-compose`** (v5.1.3, отдельный бинарь) |
| Kraken | **`docker compose`** (плагин v5.1.3); `docker-compose` → `command not found` |
Одна и та же версия, но на разных хостах **разный способ вызова**. Скрипты деплоя
для двух хостов нельзя копировать без правки этой строки.
---
## 10. HTTP-transport: ручная проба (streamable)
```bash
docker run --rm --network mattermost_mm-net curlimages/curl:latest \
-s --max-time 10 -X POST http://balda-executor-mcp:8890/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' -i
```
⚠️ Заголовок `Accept: text/event-stream` **обязателен** — сервер отвечает SSE.
Без него — отказ. Ответ отдаётся как `event: message\ndata: {...}`.
`GET /healthz` у MCP-сервера отдаёт `{"status":"ok","bridge_url":...,"secret_set":true}`.
---
## 10a. 🔴 Питфолл: `from __future__ import annotations` ломает FastAPI-эндпоинт
Симптом: `initialize` возвращал **422 `{"type":"missing","loc":["query","request"],"msg":"Field required"}`**,
и `mcp-session-id` в ответе отсутствовал — значит handshake не начинался вовсе.
**Причина:** модуль объявлен с `from __future__ import annotations`, поэтому аннотация
`async def mcp(request: Request)` превращается в **строку** и FastAPI не распознаёт
её как объект запроса → считает `request` обычным query-параметром.
**Фикс:** убрать `from __future__ import annotations` из модуля, где объявлены
FastAPI-роуты. Никакие обходы (`request: Request = None`, ручная проверка `isinstance`,
импорт `Request as FastAPIRequest`) **не помогают** — только удаление строки.
📌 Правило: файл с FastAPI-эндпоинтами не должен использовать отложенные аннотации.
Если `from __future__ import annotations` нужен для остального кода — выносить роуты
в отдельный модуль.
## 10b. 🔴 Питфолл: `command:` в compose не переопределяет ENTRYPOINT
Образ имеет `ENTRYPOINT ["python3", "/app/server.py"]`. В compose было
`command: ["python3", "/app/server.py", "--http", ...]` → аргументы **добавляются**
к ENTRYPOINT, и получается `python3 /app/server.py python3 /app/server.py --http ...`:
```
server.py: error: unrecognized arguments: python3 /app/server.py
```
**Фикс:** в `command:` указывать **только флаги**, без повторения ENTRYPOINT:
```yaml
command: ["--http", "--host", "0.0.0.0", "--port", "8890"]
```
Симптом коварен: контейнер уходит в `Restarting (2)`, а в логах — usage от argparse.
## 10c. Питфолл: `uvicorn`/`fastapi` нужны в образе для `--http`
Образ изначально ставил только `httpx` (для stdio-режима). Запуск с `--http` падал:
`ModuleNotFoundError: No module named 'uvicorn'`. Оба режима живут в одном файле,
поэтому `requirements.txt` обязан содержать `httpx`, `uvicorn` **и** `fastapi`.
## 10d. Питфолл: точка монтирования отсутствует в docker-сети
`http://balda-executor-mcp:8890` резолвится только внутри `mattermost_mm-net`.
Проверка извне сети даёт «мост недоступен» с `httpx.HTTPError`. Прогонять пробу
через `docker run --network mattermost_mm-net curlimages/curl:latest`.
⚠️ При ручной пробе через `docker run -e BRIDGE_SECRET=...` редакция секретов Hermes
может подставить `***` (3 символа) → мост вернёт 401, а `r.json()` упадёт с
`JSONDecodeError: Expecting value: line 1 column 1`. Передавать секрет через
`--env-file`, а не через `-e` в командной строке.
---
## 10e. 🔴🔴 ГЛАВНЫЙ ПИТФОЛЛ: тип транспорта — `http`, а НЕ `streamable-http`
Самый дорогой урок этой сессии. Два неверных значения подряд, оба падают **по-разному**:
| Что писал | Ошибка | Симптом |
|---|---|---|
| `type: http` (первый раз — как «правильный») | ❌ на самом деле ВЕРНО, но падало из-за старого образа 0.1.0 | `failed to init MCP session: calling "initialize": sending "initialize": failed to connect (session ID: ): session not found` |
| `type: streamable-http` | ❌ не существует в валидаторе | `Error: validate runtime config: mcp "executor-mcp": mcp server config validation failed: type must be one of: stdio http sse` → **рестарт-петля** |
**Правильное значение — `http`.** Внутренний маппинг (`internal/apps/balda/mcpfx/registry.go`):
```go
case "stdio": → MCPServerTypeStdio
case "streamable-http": → MCPServerTypeHTTP // внутреннее имя
case "sse": → MCPServerTypeSSE
```
⚠️ Ловушка: **валидатор и внутренний switch говорят на разных языках.** Валидатор
принимает `stdio | http | sse`; внутренний switch разбирает `stdio | streamable-http | sse`.
Пользователь видит только сообщение валидатора → писать надо `http`.
Не доверяй названию из кода — доверяй тексту ошибки валидатора.
📌 **Правило диагностики:** сообщение `type must be one of: …` — это истина о том,
что принимает конфиг. Читать его буквально, не додумывать «правильный» тип.
---
## 10f. 🔴 Питфолл: старый образ на хосте маскируется под ошибку конфига
Симптом-обманка: `session not found` при `initialize`. Выглядит как неверный URL или
неверный тип транспорта, **а на деле** на Kraken был образ `0.1.0`, в котором нет
`uvicorn` → HTTP-режим вообще не стартовал → порт `8890` слушал... ничего.
**Как отличить:**
```bash
# 1. Молча падает без логов? Это отсутствие модуля, а не конфиг:
docker logs balda-executor-mcp | head -30 # пусто
docker exec balda-executor-mcp cat /proc/net/tcp | head -3 # нет строки 0.0.0.0:22BA (=8890)
# 2. Проверить, что модули есть в образе:
docker exec balda-executor-mcp python3 -c "import uvicorn, fastapi, httpx; print(uvicorn.__version__)"
```
`0x22BA` — это `8890` в hex. Строка `0: 00000000:22BA … 0A` в `/proc/net/tcp` = порт слушается.
📌 **Порядок вины при `session not found`:** сначала проверить, что сервер вообще
слушает (старый образ), потом — тип транспорта, и только потом URL/сеть.
---
## 11. Что осталось
1. ✅ **`runtime.mcp_servers.executor-mcp` → `type: http`** — сделано, залито на Kraken.
2. ✅ **`obsidian` прописан рядом** (`type: sse` → `http://obsidian-mcp:3101/sse`).
3. ✅ Образ обновлён до `0.2.0` на Kraken, `tools/list` отдаёт 3 инструмента из docker-сети.
4. 🟡 Перезапустить Балду и убедиться, что инструменты обоих серверов видны в сессии
(последняя правка `type: http` в конфиге **залита в `/tmp/balda-cfg/config.yaml`**, но
на момент окончания сессии перезапуск с валидным типом ещё не подтверждён сквозным тестом).
5. 🔴 Сквозная приёмка: `executor_run` из чата → задача на Mac → результат.
6. 🔴 Логин CLI внутри `balda-executor` — делает **Alex вручную**.
## 12. Версии образа
| Тег | Что | Замечание |
|---|---|---|
| `0.1.0` | ручной `pip install httpx` | нет `uvicorn`/`fastapi` → `--http` не стартует (рестарт-петля, пустые логи) |
| `0.2.0` | `requirements.txt` (httpx + uvicorn + fastapi) | рабочий HTTP-режим; **залит на Kraken, контейнер пересоздан** |
Сборка и доставка (проверенный рецепт):
```bash
# Mac: buildx arm64 + БЕЗ provenance (иначе manifest-list ломает docker load)
docker buildx build --platform linux/arm64 -t balda-executor-mcp:0.2.0 --provenance=false --load .
docker save balda-executor-mcp:0.2.0 -o /tmp/balda-executor-mcp-0.2.0.tar
shasum -a 256 /tmp/balda-executor-mcp-0.2.0.tar # сверить после scp!
scp /tmp/balda-executor-mcp-0.2.0.tar kraken:/tmp/
ssh kraken 'docker load -i /tmp/balda-executor-mcp-0.2.0.tar'
```
Образ `0.1.0` на Kraken **не удалять** — оставлен для отката.
---
## Связанное
- [[personal/plans/balda-mm-phase4-agents]] — план Фазы 4 (шаг 5)
- [[personal/tech/agents-bridge]] — мост :8790, к которому ходит этот MCP
- [[personal/projects/balda/balda-kraken-mm-deploy]] — деплой Балды на Kraken (конфиг, `.env`, сети)
- [[personal/tech/macos-docker-virtiofs-mount-limits]] — ограничения macOS Docker
- `~/Developer/balda-executor-mcp` — код
- скилл `native-mcp` — handshake streamable-http, питфоллы