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

551 lines
30 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.
> [!WARNING] Историческая схема Balda
> Не использовать пути, контейнеры, provider или MCP transport из этой заметки для текущего запуска.
> Канонический текущий runbook: [[personal/projects/balda/setup|Balda — актуальный локальный запуск]].
> Актуально: `balda-mm-mac`, `/Users/admin/Docker/balda-mm-mac/config/config.yaml`, Mattermost, Codex ACP, Obsidian `type: http` → `http://host.docker.internal:3101/mcp`.
# balda-executor-mcp — MCP-сервер делегирования задач (шаг 5 Фазы 4)
> Создан 2026-09-26. Шаг 5 плана [[personal/plans/balda-mm-phase4-agents]].
> Статус: **собран, задеплоен на Kraken, образ `0.2.0`; конфиг Балды — `type: http` +
> `obsidian: sse`. Балда стартует без ошибок и отвечает в чате.**
> 🔴 **Блокер:** MCP-инструменты не попадают в сессию на Mattermost. Корень найден —
> **§10h** (секция провайдера + `session_memory` ломает `/reset`). Ждёт решения Alex (A/B).
> ⚠️ Прежняя версия заметки указывала на снапшот каталога — **диагноз был неверным**, §10g.
> Связка: Балда (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/сеть.
---
## 10g. 🔴 Диагностика «MCP не вижу»: снапшот каталога — ПРОВЕРИТЬ, но это НЕ корень
Симптом: **все транспорты верны, оба сервера отвечают 200 из docker-сети, а Балда в чате
говорит «MCP не вижу, ни одного сервера».** Ошибок в логах нет. `/reset` при этом
**перечисляет** серверы — то есть ID прочитаны из YAML, но инструменты не подключены.
> ⚠️ **Важно.** В первой редакции этой заметки корнем был объявлен снапшот каталога
> («в `state.db` только `builtin` → чистить снапшоты»). **Это оказался неверный диагноз.**
> Проверка показала, что снапшот в порядке и оба источника в нём есть. Удалять данные
> НЕ нужно. Настоящий корень — §10h ниже.
### Шаг 1. Подтвердить, что дело не в транспорте
```bash
docker logs balda-mm 2>&1 | grep -ci "obsidian" # → 0
docker logs balda-mm 2>&1 | grep -ci "executor-mcp" # → 0
```
### Шаг 2. Прочитать снапшот каталога (ожидаемо — он ЗДОРОВ)
⚠️ Только `sqlite3` **с хоста Kraken** (в контейнере Балды нет ни shell, ни sqlite3).
`strings` для разбора структуры НЕ годится.
```bash
STATE=/srv/dev-disk-by-uuid-49e8f586-…/docker/balda/state
DB="$STATE/state.db"
sqlite3 "$DB" "SELECT key, substr(updated_at,1,25) FROM balda_app_kv
WHERE key LIKE '%catalog_snapshot%' ORDER BY updated_at DESC LIMIT 1;"
sqlite3 "$DB" "SELECT value_json FROM balda_app_kv WHERE key LIKE '%<hash>%';" | jq '.sources, .mcp_servers'
```
**Наблюдаемый (исправный) результат:**
```json
"sources":[{"kind":"builtin","name":"balda"},
{"kind":"configured-mcp","name":"executor-mcp"},
{"kind":"configured-mcp","name":"obsidian"}]
"mcp_servers":[{"name":"executor-mcp","transport":"streamable-http"},
{"name":"obsidian","transport":"sse"}]
```
Если источники и транспорты видны — каталог исправен, идти в §10h.
### Схема таблиц (пригодится)
| Что | Где |
|---|---|
| KV-хранилище снапшотов | `balda_app_kv` (`namespace='balda.app'`, `key='runtime_catalog_snapshot:<hash>'`, значение в `value_json`) |
| Сессии | `balda_runtime_sessions` (`session_id`, `state_json`, `updated_at`) — **поля snapshot в `state_json` нет** |
| Ревизии каталога на диске | `state/catalog-revisions/` — `drwx------ root`, обычным пользователем **не читается** (`Permission denied`) |
### Механика (по коду, `internal/apps/balda/`)
| Звено | Файл | Что делает |
|---|---|---|
| параметры модуля | `catalogapp/module.go` | `params.Norma.MCPServers` ← `runtime.mcp_servers` |
| сборка | `catalogapp/runtime.go:81` | `configuredMCP: configuredMCPSources(configured)` |
| компиляция | `runtime.go:128` `PreparePluginCandidate` | `sources := [builtin] + r.configuredMCP + skills + plugins` |
| публикация | `runtime.go:168` `PublishCandidate` | `persistSnapshot` → `store.PublishApplication` → **`r.mcp.Reconcile(ctx, retained)`** |
| привязка к сессии | `runtime.go:188` `ResolveEffectiveSnapshot` | если у сессии есть `record.RuntimeSnapshotID` → берётся **именно он** |
| выдача ID | `module.go:57` `AcquireMCPServerIDs(ctx, snapshot.ID)` | ID MCP берутся из **снапшота**, не из конфига |
🔴 **Два независимых условия, оба обязаны выполниться:**
1. `PublishCandidate` должен быть **вызван** — он единственный, кто дёргает
`mcp.Reconcile`. Вызывается при публикации кандидата (плагины/скиллы/старт).
2. Сессия **не должна** быть закреплена за старым `RuntimeSnapshotID`.
Привязка вечная: строка `record.RuntimeSnapshotID != ""` → старый снапшот навсегда.
📌 **Из этого следует:** правка `runtime.mcp_servers` в YAML **не влияет** на уже
существующие сессии. Нужно новое состояние каталога + новые сессии.
### Правило для будущих правок MCP в Балде
1. Залить конфиг.
2. **Прописать серверы в секции ПРОВАЙДЕРА** — см. §10h, это обязательное условие.
3. Создать **новую** сессию.
4. Проверить **постом от админского токена** (не от бота — свои посты Балда игнорирует):
```bash
docker exec mattermost mmctl --local token generate admin whale-check --json | jq -r '.[0].token'
```
---
## 10h. 🔴🔴 НАСТОЯЩИЙ КОРЕНЬ (найден 2026-09-26): две причины, обе исправлены
### Причина 1 — провайдер не ссылался на MCP-серверы
Официальная дока (`docs/reference/configuration.md`):
> MCP servers are configured in `runtime.mcp_servers` and **referenced by providers via
> `runtime.providers.<id>.mcp_servers`**.
Формула из той же доки (стр. 555):
```
effective MCP IDs = bundled defaults + runtime.providers.<provider_id>.mcp_servers + balda.mcp_servers
```
Я добавлял ID **только** в `balda.mcp_servers` — этого мало: секция провайдера подключает
серверы к рантайму, а `balda.mcp_servers` лишь дополняет список в сессии.
**Исправлено:**
```yaml
runtime:
providers:
deepseek:
type: openai
openai:
base_url: "https://api.deepseek.com/v1"
api_key: "${DEEPSEEK_API_KEY}"
model: "deepseek-chat"
mcp_servers: # 🔴 ОБЯЗАТЕЛЬНАЯ секция
- executor-mcp
- obsidian
```
### Причина 2 — 🔴 `session_memory` включает MCP-биндер, но на Mattermost ломает `/reset`
**Включатель MCP-инструментов в сессии — это `balda.session_memory.enabled`, НЕ `balda.memory`.**
Две разные секции, легко перепутать.
Механика (`internal/apps/balda/sessionapp/module.go:191-196`):
```go
fx.Annotate(
func(broker *sessionmemorymcp.ContextBroker, enabled bool) baldaagent.SessionMCPBinder {
return SessionMCPBinderAdapter{Broker: broker, Enabled: enabled}
},
fx.ParamTags(``, `name:"balda_session_memory_enabled"`),
)
```
`enabled` = `cfg.Balda.SessionMemory.Enabled`. Далее в `agent/runtime_manager.go:308-311`:
```go
bindingEnabled := binder != nil && registry != nil
if status, ok := binder.(sessionMCPBinderStatus); ok && !status.SessionMCPEnabled() {
bindingEnabled = false // ← MCP-инструменты отключаются целиком
}
```
**НО** при `session_memory.enabled: true` сброс сессии падает:
```json
{"level":"warn","component":"balda.session_manager",
"error":"observe reset boundary: no session-memory classifier is registered for \"mattermost\"",
"boundary_reason":"reset"}
```
В чате: **`Could not reset this session.`**
Причина: `ScopeResolver` (`preflight.go`) регистрирует классификаторы **только** для
`telegram`, `slackagent`, `zulip`:
```go
sessionmemoryapp.NewScopeResolver(map[string]sessionmemoryapp.ScopeClassifier{
baldatelegram.ChannelType: baldatelegram.ClassifyLocatorScope,
baldaslackagent.ChannelType: baldaslackagent.ClassifyLocatorScope,
baldazulip.ChannelType: baldazulip.ClassifyLocatorScope,
})
```
**Mattermost в этом списке НЕТ.**
### Итоговый вывод (замкнутый круг)
| `session_memory` | MCP-инструменты | `/reset` на Mattermost |
|---|---|---|
| `false` (текущее) | ❌ не подключаются | ✅ работает |
| `true` | ✅ подключились бы | ❌ «Could not reset this session.» |
**MCP-инструменты в сессии Балды на Mattermost сейчас НЕДОСТИЖИМЫ** — ограничение апстрима,
не ошибка конфигурации. `session_memory` оставлен **выключенным** (иначе ломается `/reset`).
### Что осталось для разблокировки (нужно решение Alex)
- **A)** Добавить `ClassifyLocatorScope` для Mattermost в `ScopeResolver` (патч апстрима,
~20 строк по образцу zulip) → MCP заработает в Mattermost. **Рекомендовано** — вся работа
Alex идёт в Mattermost.
- **B)** Перевести бота на канал, где классификатор есть (`telegram` / `slackagent` / `zulip`)
→ MCP заработает без правок кода.
⛔ **НЕ делать:** чистить `state.db` / ключи снапшотов. Каталог исправен (§10g),
данные трогать не нужно.
### Питфоллы тестирования (сожрали время)
| Питфолл | Симптом | Как правильно |
|---|---|---|
| Пост отправлен от имени **бота** | `dropping posted event: bot not mentioned` — Балда молчит | слать токеном **админа**: `mmctl --local token generate admin …` |
| Забыт `@balda` в тексте | то же `bot not mentioned` | всегда упоминать бота |
| Правка `.env`, а не plist/конфига | «перезапустил, но старое значение» | смотреть, откуда реально читается |
| `docker logs` сразу после `up` | логи пусты (буферизация) | подождать ~60-80 с (OnStart ~47 с) |
| Ожидание, что `/reset` пересоздаст сессию при выключенном `session_memory` | «серверы в списке, а инструментов нет» | серверы в списке ≠ инструменты подключены |
---
## 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. ✅ Балда перезапущена с валидным типом — **стартует без ошибок**, `Balda bot started`,
`type: http` принят валидатором. Сквозной пост отработан, Балда **ответила**.
5. 🔴 **Блокер:** Балда отвечает «MCP не вижу». Корень найден (**§10h**): провайдер не
ссылался на серверы (исправлено) **+** `session_memory.enabled` ломает `/reset` на
Mattermost (`no session-memory classifier for "mattermost"`) → включатель MCP держать
выключенным. Нужно решение Alex по разблокировке (вариант A или B, §10h).
⛔ `state.db` НЕ чистить — каталог исправен (§10g).
6. 🔴 Сквозная приёмка: `executor_run` из чата → задача на Mac → результат.
7. 🔴 Логин CLI внутри `balda-executor` — делает **Alex вручную**.
### Что проверено живьём (2026-09-26, конец сессии)
| Проверка | Результат |
|---|---|
| `executor-mcp:8890/mcp` из docker-сети | HTTP 200 + `mcp-session-id` ✅ |
| `obsidian-mcp:3101/sse` из docker-сети | 200 + `text/event-stream` + `event: endpoint` ✅ |
| `executor-mcp:8890/sse` (не тот эндпоинт) | 404 — правильно ✅ |
| `obsidian-mcp:3101/mcp` (не тот эндпоинт) | 400 — правильно ✅ |
| туннель `agents-bridge.qentra.top/healthz` из `mattermost` | 200 через Cloudflare ✅ |
| Балда после правки `type: http` | старт без ошибок, ответ в чате ✅ |
| слова `obsidian`/`executor-mcp` в логе Балды | **0** ❌ → корень в снапшоте (§10g) |
📌 **Метод проверки транспорта:** у каждого сервера ровно один рабочий эндпоинт.
Быстрый тест из `mattermost` (там есть `curl`, в образе Балды — нет):
```bash
docker exec mattermost sh -c 'curl -sS -m 8 -o /dev/null -w "HTTP %{http_code}\n" -H "Accept: text/event-stream" http://obsidian-mcp:3101/sse'
```
## 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, питфоллы