Сервер MCP отдаёт числа тому, кто потом будет о них уверенно рассуждать вслух перед человеком, который проверить не может. Это другая задача проектирования, чем API для программы, и бо́льшая её часть не про протокол.

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

1. Токен — заголовок, а не параметр инструмента

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

Заголовок модели невидим. Процитировать его нельзя, потому что его нет в контексте.

Из того же рассуждения следует, что у сервера не должно быть собственного токена. Сервер Skomi пересылает то, что прислал вызывающий, и ничего не хранит, поэтому он не может ничего такого, чего вы не могли бы уже сделать через curl. Нет режима, в котором у него есть постоянный доступ к чьим-либо данным, а это единственная версия «мы серьёзно относимся к безопасности», которую можно проверить.

2. Ничто не пишет

Каждый инструмент помечен readOnlyHint, а API за ними и так только для чтения, так что пометка — описание, а не обещание.

Сказать это прямо стоит, потому что соблазн тянет в другую сторону: помощник, который мог бы изменить настройку, показывается прекрасно. Он же означает, что неверно прочитанная инструкция даёт последствия, переживающие разговор. Чтение поправимо; запись нет, а инструменту аналитики нечего писать такого, что стоило бы этого.

3. При сбое возвращается ошибка, а не нули

Когда хранилище недоступно, инструменты падают. Они намеренно не возвращают страницу нулей.

Нули были бы ответом дружелюбнее и опаснее, потому что модель не отличит сломанную неделю от тихой недели. «Во вторник трафик упал до нуля» — фраза, которую помощник произведёт добросовестно из правдоподобно выглядящих данных, а у читателя не будет способа узнать, что сбой был в аналитике, а не на его сайте.

Ошибка — опыт хуже и правдивый. Общее правило: никогда не возвращайте значение, неотличимое от настоящего измерения, когда измерения у вас нет.

4. «Нет сравнения» вместо выдуманного числа

У каждой метрики есть изменение против предыдущего периода. Когда предыдущий период был нулевым, изменение равно null: не бесконечности, не 100%, не большому числу, которое просто отрисовывается.

Процентное изменение от нуля не определено, а любое конкретное значение, которое вы подставите, — это число, которое модель повторит как факт. Инструменты говорят «нет сравнения». Это менее приятно и единственное честное из доступного.

5. Строки не складываются в 100, и инструмент об этом говорит

Разбивка считает различных посетителей на строку. Один человек на телефоне и на ноутбуке попадает в две строки, поэтому строки складываются в больше, чем итог, а проценты не дают 100.

Не сказанное вслух, это ловушка с очень конкретной поломкой: модель, которую попросили «долю трафика по странам», услужливо нормирует строки к 100%, совершенно разумное действие со списком чисел и неверное здесь так, что дальше по цепочке этого не поймает никто.

Поэтому собственное описание инструмента это говорит: делите на итог посетителей за период из сводного инструмента, а не на сумму строк. Поправка должна жить там, куда смотрит модель, то есть в описании инструмента, а не в документации, которую прочитал кто-то другой.

6. Фильтры — это измерения, и разбор один

Имя каждого измерения работает и как фильтр, с тем же смыслом в обоих местах:

breakdown  dimension=path  filters={"country":"EE","device":"mobile"}

Один разбор означает, что эти двое не могут разойтись: фильтр, который построила модель, — ровно тот фильтр, который могла бы применить панель, а измерение, о котором она узнала, спросив, — то, по которому она может немедленно отфильтровать. Есть и инструмент, возвращающий только допустимые измерения, периоды и правила фильтров, чтобы модель могла спросить, что законно, вместо того чтобы гадать и переспрашивать.

Неудобный угол: чтобы отфильтровать по значению, которое никогда не записывалось (скажем, по визитам без кампании), передаётся __none__. Пустая строка неотличима от отсутствующего параметра после разбора запроса, поэтому пустое значение должно ехать чем-то. Такое отрастает у каждого API; честный ход — задокументировать это, а не делать вид, что форма единообразна.

Что из этого складывается

Бо́льшая часть — один принцип, применённый многократно: двусмысленный ответ хуже отказа, потому что тот, кто потребляет ваш API, разрешит двусмысленность уверенно и вслух.

Это не особенность MCP. Просто обычный потребитель (программа, за которой стоит программист) бросил бы исключение, а этот вместо этого пишет абзац.

Сервер Skomi живёт на mcp.skomi.com, и ставить ничего не надо; API запросов, который он читает, — тот же самый, которым может пользоваться что угодно другое.

Числа, которые он читает, — те же самые, что показывает вам Analytics, через тот же API запросов: второго конвейера, в котором модель могла бы ошибиться, нет. Сервер MCP определяет термин, если он вам в новинку.