Что я понял, пока делал MCP-сервер для инструмента разработчика

MCP

Model Context Protocol (MCP) — стандартный способ дать ИИ-агенту набор инструментов. Вы описываете, что умеет ваш продукт, клиент агента подключается, и дальше агент вызывает эти инструменты прямо в разговоре — так же, как читает файл или запускает команду.

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

Ниже — решения, которые оказались важными, когда я выпускал MCP-сервер BugDump. Они подходят любому инструменту разработчика, который собирается обзавестись своим.

Для начала: что меняет MCP

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

С MCP агент сам достаёт то, что ему нужно. В BugDump это разница между таким запросом:

Вот баг-репорт. [вставляет 200 строк вывода консоли]

и таким:

Посмотри задачу #42 в BugDump и исправь её.

Во втором случае агент сам получает задачу, смотрит список её отчётов, читает консоль и сетевые запросы, сверяет стек вызовов с исходниками в репозитории и предлагает изменение. Интерфейс BugDump никто не открывал. Агенту достался весь отчёт, а не та часть, которую человек счёл нужным скопировать. А понять, какая часть отчёта важна, — это как раз то, что должна дать отладка, а не то, что ей нужно на входе.

В этом и выигрыш. Поэтому цель проектирования — не «открыть доступ к API», а «сделать API, в котором агент сориентируется без карты».

1. Привяжите ключ к одной области доступа, раз и навсегда

В моей первой версии у каждого инструмента был параметр проекта. Так получалось при прямом переносе REST API, и это было ошибкой дважды.

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

Ошибкой с точки зрения удобства. Если у агента есть параметр projectId, но нет ID проекта, он попробует его заполнить. Угадает proj_1. Передаст название проекта. Придумает что-то правдоподобное, получит ошибку и придумает что-то другое. Параметр, который можно угадать, будут угадывать.

Поэтому MCP-ключ BugDump навсегда привязан ровно к одному проекту, и ни один инструмент не принимает проект аргументом. Угадывать нечего. Если пользователь спросит о другом проекте, сервер может только сказать, к какому проекту привязан ключ, — через инструмент get_current_project, который существует только для того, чтобы агент сориентировался, а не строил догадки.

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

2. Проектируйте цепочку вызовов явно

Инструменты BugDump выстроены в продуманную последовательность:

Plain Text
list_tasks            → find a task, get its taskId
get_task              → read one task
list_reports_for_task → list a task's reports, get each reportId
get_report            → read one report
get_report_telemetry  → page through everything in that report

Каждый ID, который передаётся инструменту, должен прийти из предыдущего ответа. Так, этими словами, и написано в инструкциях сервера, вместе с явным запретом собирать ID самому.

Ошибка, от которой это защищает, незаметная и дорогая. Агенты охотно распараллеливают вызовы, и чаще всего правильно делают. Но агент, который решит разом запустить несколько get_report, ещё не получив ни одного ID отчёта, эти ID сочинит, получит семь ошибок и сожжёт много контекста, пока будет из них выбираться. Лечит это инструкция, в которой сказано: параллельные вызовы безопасны только с теми ID, которые у вас уже есть.

3. Принимайте идентификаторы, которыми пользуются люди

У задачи в BugDump есть UUID и публичный ID, который показывается как #42. Пользователи говорят «задача 42». Никто ни разу не сказал «задача 9f8c1a4e-...».

get_task принимает и то и другое. Уступка маленькая, а эффект несоразмерно большой: исчезает целый класс неудачных первых попыток. Агент пробует то, что сказал человек, и это работает.

Найдите идентификатор, который ваши пользователи пишут в Slack. Его и принимайте.

4. По умолчанию отдавайте сводку, но говорите, что вырезали

В одном баг-репорте могут быть тысячи записей консоли, сотни сетевых запросов с телами, скриншоты и воспроизведение сессии. Если отдавать всё это по умолчанию, первый же вызов забьёт контекст агента, и места для самой работы не останется.

Поэтому get_report по умолчанию возвращает компактную сводку: последние записи консоли и метаданные сетевых запросов без тел.

Самое важное — следующая фраза в ответе. Сводка сообщает, что из неё вырезано. Не молча, а явно, прямо в ответе.

Молча обрезать данные — худшее, что может сделать автор инструмента, потому что ошибки при этом не возникает. Возникает уверенный неверный ответ. Агент, получивший обрезанный сетевой лог без пометки, что он обрезан, скажет пользователю, что упавших запросов не было. Что это неправда, выяснится гораздо позже.

Это та же дисциплина, что у --max-count в утилите поиска: сообщать, в какой предел упёрся. Если ваш инструмент что-то ограничивает, ограничение должно быть видно в выводе.

5. Дайте агенту способ получить остальное

Сводка допустима, только если можно прочитать и всё целиком. Для этого есть get_report_telemetry: он листает консоль, сетевые запросы с телами или действия пользователя порциями в хронологическом порядке, через offset и nextOffset, а флаг hasMore становится false, когда данные закончились.

Чтобы этим мог пользоваться агент, а не только программа, нужны два свойства.

Порции по времени, а не произвольные. Когда данные упорядочены по времени, даже частичное чтение имеет смысл. Агент, получивший первые две страницы, знает начало истории, а не случайную выборку из неё.

Явный признак конца. hasMore: false — факт, на который агент может опереться. «Массив пришёл короче лимита» — это вывод, а выводы в конце длинных циклов вызовов — как раз то место, где агенты начинают фантазировать.

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

MCP-сервер отдаёт блок инструкций, который попадает в контекст агента. Туда хочется написать маркетинговый текст. Гораздо полезнее потратить это место на то, чего не понять из схем инструментов:

  • В каком порядке вызывать инструменты

  • Запрет выдумывать ID

  • Какие вызовы можно распараллеливать, а какие нет

  • Что значит и чего не значит «в сводке ничего нет»

Последний пункт там не зря. Мои инструкции говорят примерно так: ответ со сводкой сообщает, что из него вырезано, так что «в сводке ничего нет» ещё не значит «ничего нет». Одна эта фраза предотвращает конкретный неверный вывод, к которому, как я видел, приходили агенты.

Пишите блок инструкций как записку толковому коллеге, который подключается к задаче на середине. Сигнатуры типов он прочитает сам. Ему нужно то, что иначе узнаёшь, только когда ошибёшься.

7. Только чтение — это фича

MCP-сервер BugDump даёт агенту семь инструментов, и все семь только читают. Ни один не создаёт задачу, не закрывает её и не пишет комментарий.

Это продуманное решение, а не недоделка. Сервер нужен для расследования: собрать улики, сопоставить их с кодом, предложить исправление. Вторая половина работы — та, где что-то меняется: решить, что баг настоящий, что это дубликат, что он исправлен, — держится на суждении человека.

Есть и довод попроще. Утёкший ключ только на чтение — это раскрытие данных. А если утечёт ключ на чтение и запись, которым пользуется автономный процесс, под угрозой уже целостность данных. При таком выборе, да ещё когда для сценария, под который я делал сервер, запись была не нужна, решение было очевидным.

Начинайте с чтения. Добавляйте запись, когда её потребует конкретный сценарий, и выдавайте для неё отдельные ключи.

Неудобное условие

Под всем этим лежит условие, которое к проектированию протокола отношения не имеет.

Агент не может задать уточняющий вопрос. Он не напишет автору отчёта, чтобы спросить, какой у того был браузер. У него ровно одна попытка разобраться в тех уликах, что есть, и если улик мало, он выдаст гладкий, хорошо выстроенный и целиком умозрительный ответ.

Значит, польза MCP-сервера ограничена качеством данных за ним. У BugDump это работает, потому что отчёт уже содержит консоль, сетевой лог, воспроизведение сессии, действия пользователя и всё окружение — собранные автоматически в тот момент, когда случился баг. Протокол только переносит эти данные.

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

Читайте также