Skip to main content
Glama
artgas1

yandex-direct-mcp

README.md
# yandex-direct-mcp

MCP-сервер и командная строка к API Яндекс Директа v5. **113 методов, порождённых
из машиночитаемой схемы**, узкая поверхность по умолчанию, изменение выключено.

Работает и как MCP-сервер для Claude Code, Cursor, Codex и других клиентов, и как
обычная команда — если MCP не нужен.

```bash
npx yandex-direct-api-mcp help
```

> Пакет ещё не опубликован в npm. Пока — прямо из репозитория:
> `git clone https://github.com/artgas1/yandex-direct-mcp && cd yandex-direct-mcp && npm ci && npm run build`,
> дальше `node build/index.js help`. Эту врезку убрать после публикации.

## Быстрый старт

Нужен OAuth-токен Яндекса со scope `direct:api` — https://oauth.yandex.ru/
(приложению требуется одобренная заявка на доступ к API Директа).

**MCP:**

```json
{
  "mcpServers": {
    "yandex-direct": {
      "command": "npx",
      "args": ["-y", "yandex-direct-api-mcp"],
      "env": { "YANDEX_DIRECT_TOKEN": "ваш-токен" }
    }
  }
}
```

**Командная строка:**

```bash
export YANDEX_DIRECT_TOKEN="ваш-токен"

yandex-direct-mcp catalog --service campaigns
yandex-direct-mcp describe campaigns.get
yandex-direct-mcp call campaigns.get --FieldNames Id --FieldNames Name
```

## Что этот сервер делает за вас

Не удобства. Каждый пункт — место, где прямой запрос к Директу ошибается
**молча**: ответ выглядит нормальным, ошибки нет, а число или вывод неверны.
Всё перечисленное снято прогоном живых кабинетов 09.09.2026, а не прочитано
в документации. Где замер сделан на одном кабинете, а где на двух — сказано
в самом пункте.

### Суммы приходят умноженными на миллион — всегда

```
DailyBudget.Amount = 1000000000     ← это 1000 единиц валюты счёта
Cost               = 1234500000     ← это 1234,50 единицы валюты счёта
```

Ошибка ровно в миллион раз, и она не выглядит ошибкой: число правдоподобное,
его можно сложить, поделить и построить по нему график. Заголовок
`returnMoneyInMicros`, который выключает микро-единицы в отчётах, **на обычные
службы не действует** — проверено на `campaigns`, значение не изменилось.

Сервер приводит суммы к валюте счёта и перечисляет в ответе, какие именно поля
пересчитал:

```json
"_мета": { "суммы_переведены_из_микроединиц": ["Amount", "Refund", "Spend"] }
```

### Идентификаторы объявлений не помещаются в число JavaScript

Реальный `Id` объявления из кабинета: `1920778358390530027` — девятнадцать цифр.
`JSON.parse` держит пятнадцать и превращает его в `1920778358390530000`.

И это не единичный курьёз. Замер по двум нашим кабинетам: в одном 3 таких
идентификатора, в другом 14 из 898 объявлений. Схема Яндекса объявляет 358 полей
типом `xsd:long` — то есть диапазон до 19 цифр здесь нормален по контракту.

Опасен не сдвиг, а то, как он выходит наружу: **испорченный идентификатор Директ
принимает** и отвечает `HTTP 200` с телом `{"result":{}}`. То есть отказа нет —
есть сообщение «такого объявления нет». Пустота как доказательство отсутствия.

Сервер разбирает тело так, что длинные целые остаются точными. Отдельно
проверено, что API принимает идентификатор строкой, поэтому точность держится
на всём пути — и на чтении, и на записи.

### Успех определяется телом, а не кодом ответа

| что спросили | код | что в теле |
|---|---|---|
| неверный `FieldNames` | **200** | `error_code: 8000` |
| неизвестный метод | **202** | `error_code: 55` |
| ошибка в отчёте | **400** | `error_code: "8000"` — строкой, не числом |
| отчёт поставлен в очередь | **201** | пусто, `retryIn: 1` |
| отчёт считается | **202** | пусто, `retryIn: 10` |

Один и тот же код `202` означает отказ у `campaigns` и «ещё считается» у
`reports`. Проверка `res.ok` пропускает первые три строки таблицы: отказ уходит
модели как удачный ответ.

### Отчёт приходит не сразу

`201` → `202` → `200`. Замер: три запроса, 15 168 строк. Повтор идёт с тем же
`ReportName` — имя и есть ключ поставленной задачи. Сервер ждёт сам.

### Версия пути меняет данные

Один и тот же запрос:

```
/json/v5/campaigns    → 52 кампании, у всех Type = TEXT_CAMPAIGN
/json/v501/campaigns  → те же 52,     у всех Type = UNIFIED_CAMPAIGN
```

Это разные представления с разными наборами глубоких полей, и несовпадение
отнимает их без всякого признака:

| путь | набор полей | глубокие поля |
|---|---|---|
| `v5` | `TextCampaignFieldNames` | приходят |
| `v5` | `UnifiedCampaignFieldNames` | **пусто, ошибки нет** |
| `v501` | `TextCampaignFieldNames` | **пусто, ошибки нет** |
| `v501` | `UnifiedCampaignFieldNames` | приходят |

Стратегия, настройки, счётчики просто отсутствуют — читается как «у кампании
ничего не настроено». Умолчание `v501` (документация называет адресом только
его), переключается `DIRECT_API_VERSION=v5`, выбранная версия печатается в
каждом ответе, а несовпадающий набор полей вызывает предупреждение.

### Список кампаний неполон

Кампании Мастера кампаний не отдаются методом `campaigns.get` вовсе — ни
списком, ни по явному `Ids`; ответ пустой и без ошибки. Ни `v5`, ни `v501` этого
не меняют.

Поэтому состав кабинета собирает отдельный инструмент **`direct_inventory`**:
он склеивает список кампаний и отчёт и помечает каждую строку источником.
Предупреждения в описании тут мало — оно требует, чтобы читатель помнил про него
в момент вывода, а вывод делается по данным, которые выглядят нормально.

Прогон на живом кабинете за август: список отдал 52 кампании, объединение — 53,
и одна из них, с пометкой «только отчёт», собрала **60,9% всех показов месяца**.

```
ВНИМАНИЕ: 1 кампаний откручивались, но методом campaigns.get НЕ отдаются
          (706326917). Управлять ими через API нельзя — только в интерфейсе.
```

### Предупреждение — это применено, а не отклонено

В ответе на `add`/`update` каждому входному элементу отвечает выходной.
Различать надо по `Errors`; `Warnings` означает «применено с замечанием».
Счёт по наличию любого содержимого даёт «отклонено всё» там, где применилось
всё. Сервер приводит итог отдельной строкой:

```
UpdateResults: применено 2, отклонено 1, с предупреждениями 1
```

### Форму списка задаёт тип, а не направление

```
RegionIds            (maxOccurs=unbounded) → [225, 977]
RestrictedRegionIds  (тип ArrayOfLong)     → {"Items": [225]}
```

Обе формы одинаковы и на чтении, и на записи. Сервер снимает и ставит обёртку
по графу типов, а не по виду значения, поэтому круг «прочитал → поправил →
записал» не рвётся. Вам обе формы видны как обычные массивы.

### Кабинет называется в каждом ответе

`Client-Login` переключает кабинет по-настоящему, и ошибиться в нём можно молча.
Несуществующий логин отбивается кодом 8800 — это видно сразу. А существующий,
но не тот, отдаёт полные и правильные данные, просто из другого кабинета:
по виду ответа это неотличимо.

Поэтому сервер спрашивает у API, кто отвечает, и пишет ответ в каждый конверт:

```json
"_мета": { "кабинет": "neirosovaru (ClientId 316508399)", "версия_api": "v501" }
```

Спрашивается один раз за запуск и кешируется — `clients.get` стоит 10 баллов.

## Поверхность

Описания всех объявленных инструментов лежат в контексте модели **на каждом
ходу**, вызываете вы их или нет. Поэтому по умолчанию объявляется не всё, что
умеет API, а то, чем пользуются.

Замер `tools/list` на собранном сервере (`npm run surface`):

| профиль | инструментов | байт | ≈ токенов |
|---|---|---|---|
| **`core`** (умолчание) | 13 | 26 701 | 12 305 |
| `read` | 37 | 60 831 | 28 033 |
| `all` + `DIRECT_ALLOW_WRITES=1` | 117 | 143 375 | 66 071 |

Умолчание в 5,7 раза легче полного набора. Главный рычаг — вложенные типы не
разворачиваются в схему: транзитивно `campaigns.add` это 1083 поля и 54 КБ на
один инструмент. Вместо разворачивания состав типа назван словами в описании,
а точная схема выдаётся инструментом `direct_schema` по запросу.

Чего не видно — расскажет сам сервер: инструмент `direct_catalog` перечисляет
все 113 методов и говорит, какие скрыты и как их включить.

## Изменение выключено по умолчанию

Из 113 методов **80 меняют данные, 16 удаляют**. У Директа нет подтверждающего
шага: `suspend` останавливает показы в момент вызова, `archive` убирает кампанию
из работы, `delete` необратим, а на другом конце — деньги.

Меняющие инструменты **не объявляются вовсе**, пока не задан
`DIRECT_ALLOW_WRITES=1`. Объявлять их и отказывать на вызове — худший вариант:
контекст за них платится полностью, а позвать всё равно нельзя.

Неизвестное имя профиля — отказ на старте, а не откат к полной поверхности:
неверная настройка ограничения не должна превращаться в отсутствие ограничения.

Есть песочница: `DIRECT_SANDBOX=1` (нужны отдельная регистрация и отдельный
токен). Факт включения печатается при старте и в каждом ответе.

## Настройки

| переменная | по умолчанию | что делает |
|---|---|---|
| `YANDEX_DIRECT_TOKEN` | — | OAuth-токен, scope `direct:api`. Обязательна |
| `YANDEX_DIRECT_LOGIN` | — | логин кабинета (**не** почта). Переключает кабинет по-настоящему: под одним токеном отдаёт другой аккаунт со своей квотой. Фактический кабинет сервер называет в каждом ответе |
| `DIRECT_PROFILE` | `core` | `core`, `read`, `all` |
| `DIRECT_TOOLS` | — | явный список служб или инструментов, побеждает профиль |
| `DIRECT_ALLOW_WRITES` | выкл. | объявить меняющие данные инструменты |
| `DIRECT_API_VERSION` | `v501` | `v501` или `v5` — меняет представление кампаний |
| `DIRECT_SANDBOX` | выкл. | песочница вместо боевого кабинета |
| `DIRECT_MAX_OUTPUT_CHARS` | `60000` | потолок ответа; усечение называется вслух |

## Откуда берутся инструменты

Ни один метод не описан руками.

| источник | что даёт | почему нужен |
|---|---|---|
| WSDL 29 служб + 3 общие XSD | состав, типы, обязательность, массивность, перечисления | единственный полный: в индексе документации нет `vcards`, `smartadtargets`, `dynamictextadtargets`, `dynamicfeedadtargets` |
| страницы документации | человекочитаемые описания | в WSDL нет ни одного `xs:documentation` |
| описано явно | служба `reports` | WSDL для неё Директ не отдаёт (404) |

Итог: **30 служб, 113 методов, 604 типа, 235 перечислений.**

```bash
npm run spec:fetch    # скачать WSDL и документацию в .cache/
npm run spec:build    # собрать spec/direct-api.json
```

⚠️ **Перечисления из схемы отстают от живого API** и поэтому не становятся
жёстким фильтром, а идут подсказкой в описание. Сверка 09.09.2026: `campaigns`
принимает `CreateTime`, `keywords` — `AutotargetingBrief`,
`AutotargetingBriefSuggests`, `AutotargetingMode`, которых в схеме нет. Фильтр
по отстающему списку запретил бы то, что API умеет, и отказ выглядел бы как
отсутствие возможности. Право решать остаётся за API.

## Проверки

```bash
npm test        # 31 тест, включая отрицательные контроли
npm run surface # замер поверхности по профилям
npm run smoke   # прогон собранного сервера против живого API (нужен токен)
```

Тесты содержат отрицательные контроли на каждый инвариант — то есть могут
упасть на том дефекте, ради которого написаны: на порче идентификатора, на
ошибке с кодом 202, на пустой схеме `get`, на пропаже службы и на чтении
предупреждений как отказов.

## Скилл — работа без MCP

Для агентов, которым MCP не нужен или недоступен:

```bash
npx skills add artgas1/yandex-direct-mcp
```

Ставит один канонический экземпляр в `.agents/skills/yandex-direct/` и
связывает его с каталогами агентов. Скилл — тонкая надстройка над той же
командой: своей логики у него нет, поэтому расходиться с сервером ему нечем.

## Если выбираете между серверами

Серверов к Директу написано много. Полезные вопросы к любому из них — те же,
что перечислены выше: приводит ли суммы из микро-единиц; переживают ли
девятнадцатизначные идентификаторы разбор; считается ли `HTTP 200` с телом
`error` успехом; ждёт ли он отчёт после `201`; отличает ли `Warnings` от
`Errors`; что делает при опечатке в имени профиля. Ответы стоят одного вызова.

## Лицензия

MIT.