Skip to main content
Glama
yagaMI-Reverse

vibe-agent-mcp

README.md
# vibe-agent-mcp

Автономный ИИ-агент для Agent API «Вайб-Маркетолог», живущий на тарифах
Claude Code (без отдельных платных API-токенов модели) — второе тестовое
задание для компании.

Первое тестовое (детерминированный линтер их генератора,
[ad-preflight](https://github.com/yagaMI-Reverse/ad-preflight)) принято на
90/100 и названо основателем «одной из самых сильных работ кампании»: он
нашёл, что 34% сгенерированных заголовков превышают лимит Яндекс.Директа
(58–59 знаков при лимите 56). Это тестовое — тот же линтер, но встроенный в
автономный цикл: сгенерировать → проверить → переформулировать →
остановиться, с MCP-сервером сверху, чтобы Claude Code мог сам дёргать API.

## Что это

- **`mcp_server/server.py`** — MCP-сервер, 8 инструментов поверх Agent API
  (5 бесплатных read-only/локальных, 3 платных write).
- **`orchestrator/run_agent.py`** — автономный цикл с бюджетным гейтом:
  генерирует объявления → прогоняет через preflight → **механически чинит
  локально** то, что чинится обрезкой (превышение длины) → если остались
  содержательные дефекты (непроверенные обещания), переформулирует `utp` и
  повторяет → останавливается по чистоте выдачи или по бюджету/итерациям.
- **`vendor/ad-preflight`** — git submodule на первый тестовый проект;
  чек-логика (`check_ad`, лимиты Директа, ст. 5 ФЗ «О рекламе», сверка
  обещаний с лендингом) переиспользуется напрямую, не переписана.
- **[cosmetics-demo-shop](https://github.com/yagaMI-Reverse/cosmetics-demo-shop)** —
  отдельный демо-магазин косметики (свой, не чужой реальный сайт), чтобы
  проверить решение не только на лендинге самой платформы. [Живой сайт](https://yagami-reverse.github.io/cosmetics-demo-shop/).

## Важные поправки к брифу (найдено на живом API, не в документации)

1. **Нет бесплатного `/generate/estimate` для `direct/*`.** Он бесплатен
   только для общей мультимедиа-фабрики (image/video/text/voice/music) и для
   `brand_id`-полей. У `direct/ads-generate` (49₽), `direct/landing-audit`
   (39₽), `direct/sitelinks` (29₽), `direct/forecast` (49₽) цена
   **фиксированная** и известна заранее из `/capabilities.direct_tools`.
   Бюджетный гейт в этом решении сравнивает потрачённое с этой таблицей цен,
   а не делает pre-flight dry-run запрос перед каждым платным вызовом.
2. **`/api/agent/brands` реально работает** (0 брендов на аккаунте), но
   `direct/ads-generate` принимает только `url`/`utp`, не `brand_id` — бренд
   не пробрасывается в генератор напрямую.
3. **`/agent/list`, `/agent/message`, `safety-check`** недоступны этому
   токену (`404 not_found`, не вопрос скоупа) — это Bitrix24-инбокс и риск-
   оценка реальных кампаний Директа, по документации «предоставляются по
   партнёрскому соглашению». Не используются в решении.

Подробности проверки — в `docs/boundaries.md`.

## Как запустить

```bash
git clone --recurse-submodules <URL этого репо>
cd vibe-agent-mcp
pip install -r requirements.txt
```

Токен: `VIBE_API_TOKEN` в окружении или файл `C:/Users/Ilay/vibe_token.txt`
(в репозиторий не коммитится, см. `.gitignore`). Пример — `.env.example`.

### Отладка без реальных денег

```bash
python orchestrator/run_agent.py --url https://vibemarketolog.ru --dry-run \
    --max-iterations 3 --out runs/test/report.md
```

`--dry-run` вообще не бьёт в платное API — гоняет весь цикл (гейт, генерация,
preflight, переформулировка, отчёт) на `fixtures/raw_response_sample.json`
(реальный платный ответ с первого тестового). Это позволяет отладить всю
логику на 0₽ перед тем, как тратить реальный баланс.

### Реальный автономный прогон

```bash
python orchestrator/run_agent.py --url <лендинг> --max-iterations 3 --audit \
    --out runs/<дата>/report.md
```

### Регистрация MCP-сервера в Claude Code

```bash
claude mcp add vibe -- python <абсолютный путь>/mcp_server/server.py
```

## MCP-инструменты (8)

| Инструмент | Стоимость | Что делает |
|---|---|---|
| `account_status` | бесплатно | баланс, дневной лимит и остаток |
| `price_table` | бесплатно | фиксированные цены `direct/*` из `/capabilities` |
| `list_brands` | бесплатно | список брендов/товаров аккаунта |
| `get_brand` | бесплатно | профиль одного бренда |
| `preflight_check` | бесплатно, локально | вердикты ГОДНО/ИСПРАВИМО/БРАК/ГАЛЛЮЦИНАЦИЯ по пакету объявлений |
| `generate_ads` | **49₽** | `direct/ads-generate` — пакет объявлений |
| `audit_landing` | **39₽** | `direct/landing-audit` — аудит лендинга |
| `generate_sitelinks` | **29₽** | `direct/sitelinks` — быстрые ссылки и уточнения (бонус, не в брифе) |

## Проверено: MCP-протокол end-to-end (01.08.2026)

`claude mcp add vibe -- python mcp_server/server.py` + `claude mcp list` →
`✔ Connected` — но это только health-check хендшейка. Сделал более строгую
проверку: поднял настоящий MCP-клиент (`mcp.client.stdio`, тот же протокол,
что использует сам Claude Code) и прогнал реальный цикл `initialize` →
`tools/list` → `tools/call`:

```
TOOLS: ['account_status', 'price_table', 'list_brands', 'get_brand',
        'generate_ads', 'audit_landing', 'generate_sitelinks', 'preflight_check']
account_status -> {"balance": 72, ..., "daily_spend_remaining": 363, ...}
price_table    -> {"direct/landing-audit": 39, "direct/ads-generate": 49, ...}
```

Все 8 инструментов видны по протоколу, вызов реально бьёт в живой API и
возвращает актуальные данные — не заглушку. Это подтверждает, что сервер
работает не только как импортируемый Python-модуль (как было проверено
раньше), а как настоящий MCP-сервер по stdio.

## Локальный фиксер длины — почему это сработало, а переформулировка нет

Прогон 31.07 (ниже) показал: просьба словами «заголовок ≤56 знаков» в `utp`
не работает. Вместо неё — механическая обрезка по границе слова для
объявлений, у которых **единственная** причина брака — превышение длины
(`orchestrator/run_agent.py:apply_local_fixes`). Ничего не генерируется
заново, ничего не оплачивается — просто пересчитывается вердикт после
обрезки. Результат на живых данных 01.08.2026 (лендинг
[cosmetics-demo-shop](https://github.com/yagaMI-Reverse/cosmetics-demo-shop)):

| Итерация | utp | стоимость | ГОДНО | ИСПРАВИМО | БРАК | ГАЛЛЮЦИНАЦИЯ | починено локально |
|---|---|---|---|---|---|---|---|
| 1 | (пусто) | 49₽ | 135 | 3 | 0 | 9 | **81** |
| 2 | «упоминай только реальные скидки/сроки/гарантии с лендинга» | 49₽ | 136 | 11 | 0 | 0 | 66 |

Итерация 1: из 147 объявлений фиксер локально почистил 81 (были бы БРАК из-за
длины) — остались только 9 содержательных ГАЛЛЮЦИНАЦИЯ (выдуманные
подробности, не обрезкой не почини). Итерация 2: переформулировка `utp`
для этого класса дефекта **сработала** — 0 галлюцинаций. Итог: **0 БРАК,
0 ГАЛЛЮЦИНАЦИЯ, 2 итерации вместо 3, 137₽ вместо безуспешных 186₽**, и
впервые цикл реально сходится к чистой выдаче, а не просто честно
останавливается по лимиту с браком на руках.

Полные материалы: [`runs/2026-07-31_cosmetics-demo/`](runs/2026-07-31_cosmetics-demo/).

Честно: обрезка по границе слова иногда обрывает мысль на полуслове
(«…нейросетей для фото баннеров и креативов в одном» вместо «…в одном
месте») — грамматически и по лимитам корректно, но не отредактировано
человеком. Для реального запуска кампании это годится как «прошло технический
контроль», не как финальная редактура текста.

## Результат первого живого прогона — 31.07.2026 (до фиксера)

Полные материалы: [`runs/2026-07-31_live/`](runs/2026-07-31_live/) —
[report.md](runs/2026-07-31_live/report.md),
[console_log.txt](runs/2026-07-31_live/console_log.txt) (полный stdout прогона).

Лендинг: `vibemarketolog.ru`. Бюджет: 3 генерации + 1 аудит (186₽ из 453₽
на балансе). Баланс до прогона — 453₽, после — 267₽ (проверено через
`/balance` независимо от лога прогона): списано ровно 186₽ = 3×49 + 39.

| Итерация | utp | стоимость | ГОДНО | ИСПРАВИМО | БРАК | ГАЛЛЮЦИНАЦИЯ |
|---|---|---|---|---|---|---|
| 1 | (пусто) | 49₽ | 45 | 12 | 90 | 0 |
| 2 | «каждый заголовок ≤56 знаков…» | 49₽ | 32 | 4 | 111 | 0 |
| 3 | «каждый заголовок ≤56 знаков…» | 49₽ | 15 | 0 | 153 | 0 |

Остановка: исчерпаны 3 итерации, брак остаётся (программа честно не
рапортует успех, которого не было).

**Честная находка**: переформулировка `utp` с текстовой инструкцией про длину
заголовка не снизила брак, а увеличила его — 90→111→153. Разбор причины и
вывод — в `docs/boundaries.md`, раздел 1. Это не баг цикла: гейт и критерий
остановки отработали корректно (не зациклились, не приукрасили результат),
проблема в самой стратегии «попроси генератор словами исправить формат» для
этого типа дефекта.

**Что сделано с этой находкой дальше**: не выброшена, а исправлена — см.
раздел выше «Локальный фиксер длины». Переформулировка `utp` осталась только
для содержательных дефектов (галлюцинации), для длины теперь механическая
обрезка. На новом прогоне это дало 0 БРАК за 2 итерации вместо честного
поражения за 3.

## Бонус: `direct/sitelinks` (29₽) — вне брифа, для полноты MCP-инструментов

Живой вызов на том же лендинге вернул 8 корректных sitelinks (title≤30,
description≤60) — сырой ответ: [`runs/2026-07-31_live/raw/sitelinks.json`](runs/2026-07-31_live/raw/sitelinks.json).

**Честно про этот вызов**: он оплачен дважды (58₽ вместо 29₽) — я тестировал
скрипт демо-вызова, он упал на отсутствующей папке `runs/…/raw/` уже ПОСЛЕ
успешного платного POST, я пересоздал папку и перезапустил, оплатив ещё раз.
Баланс упал 267₽ → 209₽ вместо ожидаемых 267₽ → 238₽. Вывод для реального
использования: любой скрипт с платным вызовом обязан гарантировать
существование выходной директории (или писать во временный файл) **до**
запроса, а не полагаться на то, что запись после него не упадёт. Мелочь, но
именно так на реальных деньгах теряются 29₽ по невнимательности — а не из-за
API.

## Границы решения

Полный честный список — [`docs/boundaries.md`](docs/boundaries.md): что
проверено вживую, что нет, где решение сломается и почему.

---

Илья Шаповалов · [ilya-proof.vercel.app](https://ilya-proof.vercel.app) ·
[github.com/yagaMI-Reverse](https://github.com/yagaMI-Reverse)