# Yandex Metrika MCP Server MCP-сервер к API Яндекс Метрики. Покрыты все 108 методов; по умолчанию объявляются **десять** — те, которыми считают. Остальное включается одной переменной. mcp-name: io.github.artgas1/yandex-metrika-mcp-server [![npm](https://img.shields.io/npm/v/yandex-metrika-mcp-server)](https://www.npmjs.com/package/yandex-metrika-mcp-server) [![CI](https://github.com/artgas1/yandex-metrika-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/artgas1/yandex-metrika-mcp/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE) *[English](./README.en.md)* Вопрос «Откуда приходили люди за неделю и сколько дошло до цели?» и ответ таблицей: Поиск — 12 480 визитов, 386 целей, конверсия 3,1%; Реклама — 2 140 и 5,5%; прямые заходы — 1 905 и 2,3%; переходы по ссылкам — 640 и 1,9%. Числа иллюстративные. ```bash npx -y yandex-metrika-mcp-server ``` Форк [atomkraft/yandex-metrika-mcp](https://github.com/atomkraft/yandex-metrika-mcp) (апстрим — Vadim Bezymianyi, MIT). С версии 2.0.0 инструменты не пишутся руками, а порождаются из спеки, собранной по официальной документации. ## Покрытие | API | методов | из них в профиле `core` | примеры инструментов | | --- | ---: | ---: | --- | | Management | 95 (21 ресурс) | 4 | `metrika_counter_list`, `metrika_goal_create`, `metrika_segment_update` | | Logs | 7 | — | `metrika_logs_create`, `metrika_logs_get`, `metrika_logs_download` | | Stat | 6 | 6 | `metrika_stat_data`, `metrika_stat_bytime`, `metrika_stat_pivot` | Имя инструмента — `metrika_<ресурс>_<действие>`, где ресурс взят из URL самого API без переименований. Поэтому `metrika_goal_list` однозначно отображается в `GET /management/v1/counter/{id}/goals` и в свою страницу документации. ## Контракт Сервер переписан из-за двух наблюдавшихся отказов: он отдавал не то, что просили, и молча подмешивал фильтр. Отсюда четыре правила, каждое закрыто тестом. 1. **Никакой молчаливой подмены.** Что попросили — то и уходит в API. Сервер не досочиняет ни измерений, ни периода, ни фильтров. 2. **Всё, что сервер добавил от себя, видно в ответе.** Ответ приходит как `{"_meta": {...}, "data": {...}}`, где `_meta.applied_by_server` перечисляет добавленное, а `_meta.notes` — принятые за вызывающего решения. 3. **Отказ остаётся отказом.** Ошибка API возвращается с `isError: true` и телом ответа Метрики. Повтор делается по статусу (429/500/502/503/504 и сетевые сбои), а не по подстроке в тексте; у 429 соблюдается `Retry-After` с потолком 30 секунд. Число повторов всегда видно в `_meta.retries`. 4. **Обрезание выдачи видно.** В `_meta` едут `rows_returned`, `rows_total` и `truncated` — Метрика режет ответ по умолчанию, и молчать об этом нельзя. Если сервер сам урезал ответ по потолку длины, это отдельно объявлено в `_meta.truncated_by_server` с числом выброшенных строк. 5. **Секреты не уезжают в ответ.** У `metrika_measurement_delete` есть параметр `token`; в показанном `_meta.request_url` его значение заменено на `REDACTED`. Сам OAuth-токен уходит только заголовком и в ответе не появляется никогда. ### Фильтр роботов В отчётах Stat API по умолчанию применяется собственный флаг робота Метрики, и только он: ``` ym:s:isRobot=='no' ``` Он **объявлен**: виден в схеме инструмента, отключается параметром `human_traffic_only: false` и всегда перечислен в `_meta.applied_by_server`. Если в запросе есть метрики `ym:ad:` или `ym:ev:`, фильтр не применяется (Метрика отвечает на такое сочетание 400) — и это попадает в `_meta.notes`, а не остаётся молчаливым исключением. Своё условие задаётся переменной `METRIKA_TRAFFIC_FILTER` — **целиком**, включая `isRobot`, если он нужен: ``` METRIKA_TRAFFIC_FILTER="ym:s:isRobot=='no' AND ym:s:browserName!='HeadlessChrome'" ``` Это образец формы, а не рекомендация. Какой рез верен — зависит от того, какие боты ходят именно к вам: отсечка по стране, по заголовку браузера или по подсети осмысленна только на своих данных. Копировать чужой список бессмысленно и опасно: он вырежет живой трафик. Заданное своё условие сервер называет в stderr при старте — оно меняет числа в каждом отчёте, и молчать об этом нельзя. ### Сравнение периодов: ответ, который выглядит валидным У `metrika_stat_comparison` и `metrika_stat_comparison_drilldown` даты периодов **необязательны**, и Метрика на их отсутствие не ругается. Она подставляет собственное окно (последняя неделя) в **оба** набора и возвращает сравнение периода с самим собой: ``` metrika_stat_comparison(ids, metrics) → totals a == b query date1_a == date1_b ``` Отказывать сервер не будет — запрос ушёл ровно тем, каким его собрали. Но такой ответ приходит с пометкой в `_meta.notes`: и когда даты не заданы, и когда периоды совпали явно. ## Как устроена спека Публичного `openapi.json` у Метрики нет, но каждая страница метода сгенерирована из OpenAPI движком Diplodoc и отдаётся как `text/markdown`. Семантика (тип, `required`, комбинатор, ассертация) лежит в CSS-классах вида `{.json-schema-property}`, поэтому спека собирается построчным сканером по классам, а не markdown-парсером. ```bash npm run spec:fetch # скачать llms.txt и 108 страниц в .cache/docs/ npm run spec:build # разобрать их в spec/metrika-api.json npm test # тесты спеки и схем инструментов npm run smoke # живые вызовы к API (нужен YANDEX_API_KEY) ``` `spec/metrika-api.json` коммитится — это состав API на момент сборки. Тест на дрейф сверяет его с `llms.txt`: Яндекс добавил или удалил метод — тест краснеет. Разбор привязан к версии генератора (`Diplodoc Platform v5.57.3`): вся семантика висит на его классах, поэтому расхождение версии останавливает сборку спеки, а не молча портит её. ## Запуск > **По умолчанию объявляются десять инструментов из 108** — те, которыми считают. Управление > счётчиками и целями, доступы и Logs API включаются переменной `METRIKA_PROFILE`; подробности > ниже, в разделе [«Почему по умолчанию не всё»](#почему-по-умолчанию-не-всё). > > Спросить у самого сервера тоже можно: инструмент `metrika_catalog_list` перечисляет, что > объявлено, что скрыто и как это включить. ```bash npm install npm run build YANDEX_API_KEY= npm start ``` Токен — OAuth Яндекса, тот же, что используется для Директа и Вебмастера. По умолчанию сервер сохраняет stdio-режим. Для одного локального процесса, к которому подключаются несколько MCP-клиентов, включите stateless Streamable HTTP: ```bash YANDEX_API_KEY= \ MCP_TRANSPORT=http MCP_HOST=127.0.0.1 MCP_PORT=13404 \ npm start ``` Endpoint — `http://127.0.0.1:13404/mcp`. При loopback-привязке сервер также проверяет `Host`, чтобы локальный endpoint нельзя было вызвать через DNS rebinding. ### Подключение к клиенту ```json { "mcpServers": { "yandex-metrika-mcp": { "command": "npx", "args": ["-y", "yandex-metrika-mcp-server@3"], "env": { "YANDEX_API_KEY": "..." } } } } ``` Из локальной сборки — то же самое, но `"command": "node"` и путь до `build/index.js`. Мажор в строке запуска закреплён намеренно: смена мажорной версии меняет набор инструментов по умолчанию, и получать это молча при старте агента не нужно. ### Переменные окружения | Переменная | По умолчанию | Что делает | | --- | --- | --- | | `YANDEX_API_KEY` | — | OAuth-токен. Без него сервер не стартует. | | `MCP_TRANSPORT` | `stdio` | Транспорт: `stdio` или stateless Streamable `http`. | | `MCP_HOST` | `127.0.0.1` | Адрес HTTP listener. Используется только при `MCP_TRANSPORT=http`. | | `MCP_PORT` | `3000` | Порт HTTP listener, целое число от 1 до 65535. | | `METRIKA_PROFILE` | `core` | Какая часть каталога объявляется: `core` (10 инструментов), `read` (все 51 читающих), `all` (все 108). Неизвестное значение роняет старт. | | `METRIKA_ALLOW_WRITES` | не задана | `1` разрешает и **объявляет** 57 инструментов, меняющих данные. Пока не задана — их нет в `tools/list` вовсе. | | `METRIKA_TOOLS` | пусто | Своя выборка через запятую: раздел (`stat`, `logs`, `management`), префикс имени (`metrika_goal`) или точное имя. Задана — побеждает профиль. | | `METRIKA_TRAFFIC_FILTER` | `ym:s:isRobot=='no'` | Условие сегментации, добавляемое к отчётам Stat. Задаётся целиком. | | `METRIKA_MAX_OUTPUT_CHARS` | `120000` | Потолок длины ответа одного вызова. Выгрузка Logs API в него обычно не помещается — сутки визитов это сотни тысяч символов; урезание объявляется в `_meta.truncated_by_server`. | | `METRIKA_API_BASE` | пусто | Подмена адреса API (прокси, заглушка в тестах). Факт подмены печатается в stderr. | ### Как узнать, что скрыто, не открывая README Инструмент **`metrika_catalog_list`** объявлен в любом профиле и отвечает из спеки, лежащей в пакете, — ни токена, ни сети ему не нужно: ```json { "profile": "METRIKA_PROFILE=core", "api_methods_total": 108, "api_methods_declared": 10, "api_methods_hidden": 98, "writes_enabled": false, "declared_tools": { "Stat API — отчёты": ["metrika_stat_data", "…"] }, "hidden_tools": { "Management API — …": ["metrika_goal_create", "…"] }, "how_to_widen": ["METRIKA_PROFILE=read — …", "METRIKA_PROFILE=all вместе с METRIKA_ALLOW_WRITES=1 — …"] } ``` Он существует по простой причине: **сервер, который что-то скрыл, обязан уметь сказать, что именно и как это включить.** `instructions` видит модель, но не человек — в интерфейс клиента они не показываются; стартовую строку в stderr в обычной работе тоже никто не открывает. Без этого инструмента узнать про остальные 98 можно было только придя сюда. Список инструментов в ответе строится из того же отбора, по которому они регистрируются, — разойтись с реальностью ему негде, и это проверено тестом. ### Почему по умолчанию не всё Список из 108 инструментов сервера: десять оставлены, 98 вычеркнуты. Манифест по умолчанию — 32 181 байт против 158 301 у полного каталога. Описания объявленных инструментов лежат в контексте модели, когда клиент их загрузил. Это цена сервера, которую платят за сам факт подключения, а не за вызовы. Замер `tools/list` (09.09.2026): | Профиль | Инструментов | `tools/list` | токенов | | --- | ---: | ---: | ---: | | `core` (по умолчанию) | 10 + каталог | 32 181 Б | **14,8 тыс.** | | `read` | 51 + каталог | 68 074 Б | ~31 тыс. — оценка | | `all` + `METRIKA_ALLOW_WRITES=1` | 108 + каталог | 158 301 Б | ~73 тыс. — оценка | Замер `core` — 14,5 тысячи до появления каталога и 14,8 после: сам инструмент стоит около 670 байт схемы, примерно 2% набора. Его ответ не входит в эту цену — он платится только при вызове. Байты точные, их воспроизведёт любой: сериализуй ответ `tools/list` и посчитай длину. С токенами сложнее, и здесь стоит сказать прямо. ⚠️ **Замер честный только у `core`** — его дал `/context` клиента, который считает собственным токенизатором. Две другие строки пересчитаны из байтов по калибровке **2,17 байта на токен**, снятой с той же строки `core`. Ходовая эвристика «4 символа на токен» здесь **врёт почти вдвое**: она выведена на английском тексте, а описания у этого сервера русские, и кириллица в BPE токенизируется примерно вдвое хуже латиницы. Первая редакция этой таблицы была построена именно на ней и называла для `core` 7,9k вместо 14,5k. Если считаешь бюджет контекста для сервера с не-английскими описаниями — считай токенизатором, а не делением на четыре. Состав `core` выведен из замера реального использования, а не из вкуса: шесть отчётов Stat плюс справочники, без которых отчёт не собрать (`metrika_counter_list`, `metrika_counter_get`, `metrika_goal_list`, `metrika_segment_list`). Порог веса стоит тестом — манифест не может подорожать молча. Порог в тесте стоит на **байтах**: они не зависят ни от токенизатора, ни от языка описаний. ## Безопасность - **Запись выключена по умолчанию, и меняющие инструменты не объявляются вовсе.** Среди методов четырнадцать `DELETE` и пять удаляющих `POST` (`.../measurement/delete`, `.../expense/delete`, `.../logrequest/{id}/clean` и т. д.). Цена ошибочного вызова — удалённый счётчик или цель без возможности восстановить историю. Модель не может позвать то, чего не видит в `tools/list`; как включить — сказано в `instructions` сервера. - **Аннотации проставлены на всех инструментах** (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Клиент по ним отличает чтение от удаления: удаление под глаголом `POST` помечено разрушающим, `PUT` — тоже, потому что заменяет сущность целиком. - **Ответы Метрики — недоверенные данные.** В отчётах лежат поисковые фразы, заголовки страниц, реферера и значения UTM, то есть строки, которые пишут посетители сайта. Любой может зайти на сайт по ссылке с текстом внутри и увидеть его в отчёте. У всех инструментов `openWorldHint: true`, а в `_meta.notes` отчётов и выгрузок едет напоминание, что это данные, а не инструкции. - **stdio остаётся транспортом по умолчанию.** HTTP включается только явно через `MCP_TRANSPORT=http`; безопасный дефолт слушает `127.0.0.1` и проверяет `Host`. ## Политика приватности Сервер не собирает, не хранит и никуда не передаёт данные о вас. Ни телеметрии, ни аналитики, ни обращений к серверам автора — их не существует: под этот пакет не поднято никакой инфраструктуры. Единственный сетевой адресат — `https://api-metrika.yandex.net`. Токен читается из `YANDEX_API_KEY` в память процесса и никуда не пишется: ни в файл, ни в stdout, ни в тело ответа. Данные отчётов не кэшируются на диск и не переживают процесс. Данные, которые вы запрашиваете, обрабатывает Яндекс как оператор Метрики — на это распространяется [его политика](https://yandex.ru/legal/confidential/), а не эта. Полный текст: [`PRIVACY.md`](./PRIVACY.md). ## Установка одним файлом (MCPB) Для Claude Desktop и других клиентов, понимающих MCP-бандлы, есть `.mcpb`-файл — он лежит в [релизах](https://github.com/artgas1/yandex-metrika-mcp/releases). Открываете файл, вводите токен в окне установки — всё. Бандл собирается из того же кода тем же тегом (`npm run mcpb`), а его манифест **генерируется** из `package.json` и профиля — не пишется руками, поэтому разойтись с сервером ему негде; это проверяется тестом. ⚠️ **В бандле нельзя включить запись.** Цена ошибочного вызова — удалённый счётчик или цель без возможности восстановить историю, и щёлкать таким переключателем в окне установки нечего. Нужна запись — ставьте пакет с npm и включайте её осознанно, переменной окружения. ## Без MCP: скилл и командная строка MCP подходит не всем и не всегда: клиент может не уметь MCP вовсе, а описания инструментов занимают контекст постоянно — они лежат в нём, пока сервер подключён, вызываешь ты их или нет. Для этого случая тот же сервер умеет запускаться командой: ```bash npx -y yandex-metrika-mcp-server catalog --search goal npx -y yandex-metrika-mcp-server describe metrika_stat_data npx -y yandex-metrika-mcp-server call metrika_stat_data \ --ids --dimensions ym:s:trafficSource \ --metrics ym:s:visits,ym:s:users --date1 7daysAgo --date2 today ``` Поверх этого лежит **скилл** — папка с инструкцией для агента, которая ставится одной строкой: ```bash npx skills add artgas1/yandex-metrika-mcp # в текущий проект npx skills add artgas1/yandex-metrika-mcp -g # глобально, во все проекты ``` Скилл не добавляет клиенту инструментов и ничего не держит в контексте: он читается только когда речь зашла о Метрике. Внутри — та же команда, справочник всех 108 методов и словарь измерений. **Где он работает.** Установщик кладёт один экземпляр в `.agents/skills/yandex-metrika/` и симлинкует его в папки конкретных агентов. Проверено запуском на двух: | агент | обнаружение | чем проверено | | --- | --- | --- | | Claude Code | `.claude/skills/` → симлинк | `/yandex-metrika` отвечает из содержимого скилла | | Codex | `.agents/skills/` напрямую | называет путь к `SKILL.md`; ни строки в `AGENTS.md`, ни настройки в `config.toml` для этого не нужно | Установщик заявляет ещё около двадцати агентов через тот же универсальный каталог (Amp, Cline, Antigravity, Augment и другие) — там мы не проверяли. **Почему это не вторая реализация.** CLI не делает ни одного собственного запроса: он разбирает аргументы и зовёт `executeMethod` — ту же функцию, что и MCP-инструменты. Отсюда одинаковые гарантии: фильтр роботов в отчётах, потолок ответа с распиской об урезании, вычистка секретов из показываемого URL, повтор по статусу. Разойтись им негде, потому что расходиться нечему. Справочник методов внутри скилла **генерируется** из `spec/metrika-api.json` — той самой спеки, которая обновляется из документации Яндекса ежедневно. Тест сверяет закоммиченный файл с тем, что сгенерировалось бы сейчас, поэтому «скилл отстал от API» здесь красное, а не незаметное. Два сознательных отличия команды от MCP: | | MCP | команда | | --- | --- | --- | | `METRIKA_PROFILE` | действует, по умолчанию `core` | **не действует** — доступны все 108 методов | | `METRIKA_ALLOW_WRITES` | нужен для меняющих данные | **нужен так же** | Профиль существует, чтобы не платить контекстом за описания невызванных инструментов; у команды в терминале такой цены нет. Гейт записи — про другое: удалённую цель нечем восстановить, и послабление здесь было бы дырой в обход сервера. ## Проверки ### Не макет — запустите сами ```bash npm run demo ``` Запись прогона в терминале: запрос metrika_stat_data с измерением по источникам трафика и периодом в неделю, ответ с объявленным фильтром роботов и тремя строками отчёта. Всё на записи приходит из ответа сервера по JSON-RPC: строка добавленного фильтра — из _meta.applied_by_server, строки отчёта — из тела ответа. Ни токена, ни сети: запросы уводятся на локальную заглушку, поэтому прогон повторяется где угодно, включая CI. Переснять запись — npm run demo:record. ```bash npm test # 87 тестов: спека, схемы, протокол MCP, поверхность, бандл, демо npm run protocol # только протокольные: stdio, tools/list, tools/call, отказы npm run smoke # живые вызовы к API (нужен YANDEX_API_KEY) ``` Протокольные тесты поднимают сервер как подпроцесс и говорят с ним по JSON-RPC — тем же способом, каким это делает клиент. Сеть при этом не нужна: `METRIKA_API_BASE` уводит запросы на заглушку. Проверяется в том числе то, чего не видно изнутри: что в stdout не попадает ничего, кроме JSON-RPC, что отказ API приезжает как `isError`, а не как успешный текст, и что запись действительно заблокирована. ### Чего в проверках НЕТ **Евала выбора инструмента.** Это единственная проверка, которую не заменяют ни снапшот схемы, ни протокольный тест: описания могут быть синтаксически безупречны, а модель всё равно возьмёт не тот инструмент. Тесты этого не видят по построению — они зовут инструмент по имени, то есть выбор уже сделан за модель. Здесь это осознанный пропуск, а не забытый пункт. Профиль по умолчанию — десять инструментов, из них шесть отчётов Stat различаются формой ответа, а не темой, и путать их модели особо не с чем. Евал становится нужен, когда поверхность по умолчанию расширяется или когда в неё попадают инструменты с пересекающимися описаниями, — тогда его надо писать **до** расширения, а не после. ## Что изменилось в 2.0.0 Удалены 26 инструментов-обёрток над пресетами Stat API (`get_visits`, `sources_summary`, `get_page_performance` и прочие). Они покрывали малую часть API, зашивали измерения и период в код и не давали задать произвольный запрос. Их заменяют `metrika_stat_*`, принимающие параметры Stat API как есть. Появились методы, которых не было вовсе: список счётчиков, цели, сегменты, фильтры, разрешения, расходы, офлайн-конверсии и весь Logs API. Раньше идентификатор счётчика приходилось знать заранее — теперь его можно найти. ## Что изменилось в 2.1.0 Сервер довели до состояния, в котором его не страшно оставить агенту. - **Аннотации на всех 108 инструментах.** До этого клиент не отличал `metrika_counter_list` от `metrika_counter_delete`. - **Запись выключена по умолчанию** (`METRIKA_ALLOW_WRITES`). - **Найден и починен дефект разбора документации.** Ассертации размечены строкой, где значение стоит *после* закрывающей скобки класса, — распознаватель свойств заякорен на конец строки и такие строки не матчил вовсе. В итоге до спеки не доезжало **ни одного** примера, значения по умолчанию или границы, а часть их падала в описание соседнего поля. Сейчас в спеке 288 примеров, 69 значений по умолчанию и 155 ограничений; ограничения переносятся в схему инструмента, примеры и значения по умолчанию — в описания параметров. - **Найдена и починена потеря обязательности.** Параметры вида «один из N типов» (`goal` у создания и правки цели, `grant` у выдачи доступа) собирались как `z.unknown()`, а он в zod необязателен, — обязательное поле уезжало клиенту как опциональное. Теперь это объединение реальных форм, и обязательность на месте. - **Ссылки на сущности разворачиваются** на один уровень: у 23 параметров тела вместо свободного объекта видны настоящие поля. - **Послабления на входе там, где они безвредны.** Число строкой, булево словом, список через запятую в строке запроса — принимаются; в теле запроса, где важен точный JSON, не принимаются. - **Потолок длины ответа** с объявленным урезанием: выгрузка Logs API бывает в сотни мегабайт. - **Вычистка секретов** из показанного `request_url`. - **Повтор на 429** с соблюдением `Retry-After`. - **SDK обновлён** до 1.30 — на 1.17 висели три опубликованных уязвимости, две высокие; `npm audit --audit-level=high` теперь часть CI. - **Починена джоба дрейфа в CI.** Она запускала тесты через `| tee` без `pipefail`, поэтому код возврата брался у `tee` и джоба оставалась зелёной при любом падении теста.