# 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 '%%';" | 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:'`, значение в `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..mcp_servers`**. Формула из той же доки (стр. 555): ``` effective MCP IDs = bundled defaults + runtime.providers..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, питфоллы