Блог

Как построить MCP-сервер для таск-трекера: подключаем Jira, Linear, Asana, ClickUp, Trello и Monday.com к ИИ-агентам

Лукман Нуриахметов
Лукман Нуриахметов· Основатель и CTO
mcpintegrationsarchitecture

Если ваш трекер не поставляет официальный MCP-сервер, а официальный дает только доступ на чтение, вы в итоге строите свой. Задача меньше, чем кажется. Проблема с безопасностью куда больше, чем кажется: в момент, когда ИИ-агент получает возможность вызвать create_issue или delete_task против настоящей доски, вы построили не скрипт-интеграцию, а инфраструктуру контроля доступа.

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

Что на самом деле даёт MCP

Model Context Protocol предоставляет ИИ-клиенту два вида сущностей: инструменты (функции, которые модель может вызвать, например create_issue) и ресурсы (данные, которые она может прочитать, например список задач проекта). Работает это через один из двух транспортов:

  • stdio: локальный подпроцесс, который клиент запускает сам. Подходит для машины одного разработчика.
  • Streamable HTTP: сервер, к которому подключаются удалённо другие люди и другие агенты. Именно здесь аутентификация становится вашей проблемой, а не проблемой трекера.

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

Шаг 1: сопоставить API и модель авторизации каждого трекера

У каждого трекера ниже есть рабочий REST или GraphQL API. Различаются они моделью авторизации, и именно здесь наивная интеграция обычно ломается первой.

ТрекерФорма APIАвторизацияПодводный камень
JiraREST v3API-токен + email, либо OAuth 2.0 (3LO) для мульти-тенантных приложенийПерсональный API-токен даёт доступ ко всему, что видит аккаунт, а не к одному проекту
LinearGraphQLПерсональный API-ключ или OAuth2Персональный ключ наследует все права создателя в воркспейсе
AsanaRESTПерсональный токен доступа (PAT) или OAuth2PAT не истекает сам по себе; утёкший токен остаётся рабочим, пока кто-то не вспомнит его отозвать
ClickUpREST v2Персональный токен или OAuth2Скоуп токена по умолчанию: весь воркспейс
TrelloRESTПара API key + tokenТокен генерируется на пользователя по всем доскам, которые он видит, а не под конкретную доску
Monday.comGraphQLПерсональный API-токенТокен несёт полную роль пользователя, включая админские действия, если они у него есть

Паттерн одинаков для всех шести: собственная модель авторизации трекера даёт вам credential по принципу «всё или ничего». Ни один из них не знает и не заботится о том, что вы хотите дать агенту доступ только к одному проекту. Эту границу приходится строить самостоятельно, а не получать бесплатно, выбрав «токен только для чтения», даже если трекер вообще его предлагает.

Шаг 2: проектировать схемы инструментов со строгой валидацией входа

Не поддавайтесь соблазну выставить сырой API трекера как один инструмент call_api(method, path, body). Это меньше кода, но это же выдаёт модели карт-бланш бить в любой эндпоинт, до которого дотягивается токен.

Вместо этого определяйте узкие, целевые инструменты:

server.tool(
  "create_issue",
  {
    projectId: z.string(),
    title: z.string().max(300),
    description: z.string().max(10000).optional(),
    priority: z.enum(["low", "medium", "high"]).optional()
  },
  async ({ projectId, title, description, priority }) => {
    assertProjectAllowed(projectId); // см. Шаг 3
    return trackerClient.createIssue({ projectId, title, description, priority });
  }
);

Каждое поле типизировано и ограничено. Модель не может протащить произвольный JSON-блоб в API трекера. Именно в assertProjectAllowed реально живёт контроль доступа, а не в запоздалой надстройке над HTTP-клиентом.

Шаг 3: разграничить доступ раньше, чем разграничивать функциональность

Это тот шаг, который команды пропускают, и именно он превращает «мы дали агенту доступ к трекеру» в отчёт об инциденте.

  • Токен на агента, а не один общий credential. Если три агента используют один API-токен Jira, вы не можете понять, кто из них внёс конкретное изменение, и не можете отозвать доступ одному, не отключив остальных. Выдавайте отдельное подключение на каждого агента, а в идеале – отдельный сервисный аккаунт.
  • Явный allowlist проектов, который проверяет ваш сервер, а не токен трекера. Раз токен трекера сам по себе даёт доступ «всё или ничего», ваш MCP-сервер должен сам сверять projectId со списком до того, как запрос вообще увидит клиент трекера.
  • Разделяйте read- и write-инструменты, а деструктивные прячьте за подтверждением. list_issues и delete_issue не должны проходить одну и ту же проверку прав. Инструмент delete_issue должен требовать текущую версию задачи или отдельное поле подтверждения, чтобы галлюцинированный вызов не мог молча что-то удалить.
function assertProjectAllowed(projectId: string) {
  if (!agentContext.allowedProjects.includes(projectId)) {
    throw new Error(`Agent is not authorized for project ${projectId}`);
  }
}

Эта проверка и есть разница между «агент подключён к нашему трекеру» и «агент подключён именно к тому проекту, который ему разрешено трогать».

Шаг 4: хранить и ротировать токены как production-секреты, потому что это они и есть

  • Никогда не коммитьте токен трекера в репозиторий или конфиг, который в итоге туда попадёт. Берите его из secret-менеджера в рантайме.
  • Ротируйте токен, и постройте механизм ротации до того, как он понадобится вам посреди инцидента. Токен, который нельзя быстро сменить, нельзя и по-настоящему отозвать, когда что-то пошло не так.
  • Используйте idempotency-ключи на каждом write-вызове. Если агент повторяет таймаутнувший create_issue, задача не должна создаться дважды: стабильный ключ позволяет трекеру (или вашему серверу) распознать повтор как ту же логическую операцию.

Шаг 5: рейт-лимиты и конкурентные записи

Каждый трекер из таблицы выше применяет свой рейт-лимит и возвращает 429 (или ошибку throttling на уровне GraphQL) при превышении. Стройте экспоненциальный backoff вокруг каждого write-вызова, а не как try/catch-обёртку постфактум: агент, который ретраит упавший вызов в тесном цикле, временно заблокирует доступ вашего сервера у трекера, а не только собственный запрос.

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

Шаг 6: задеплоить, не пробив дыру в собственной модели безопасности

Если вы выкатываете версию на Streamable HTTP, у самого MCP-эндпоинта должна быть своя аутентификация, независимая от трекера. У неаутентифицированного MCP-сервера с рабочим токеном Jira поверхность атаки больше, чем у самого трекера: кто угодно, кто найдёт эндпоинт, наследует всё, что может этот токен.

Где это обычно ломается

  • Один общий токен на всех агентов, без способа понять, кто что сделал.
  • Схемы инструментов, которые пропускают сырые тела запросов насквозь: валидации нет нигде.
  • Скоуп проекта, унаследованный от токена трекера, а не проверяемый в собственном сервере.
  • Механизм ротации токена, которого не существует до того дня, когда он срочно понадобится.
  • Отсутствие backoff на ретраях: застрявший агент сам себе блокирует доступ, упираясь в рейт-лимитер трекера.
  • Удалённый MCP-эндпоинт без собственной аутентификации, доступный всем, кто найдёт URL.

Как всё это можно пропустить

TAM уже поставляется как MCP-сервер, построенный вокруг всех перечисленных ограничений: каждый подключённый агент получает свой аккаунт и свой токен, со скоупом на явный список allowedTools и allowedProjects, зашитый прямо в токен и проверяемый на стороне сервера, а не прикрученный в прикладном коде, который вам пришлось бы поддерживать. Доступ отзывается по отдельному агенту, не трогая остальные подключения, активность по PR и CI привязывается к тикету автоматически, вместо того чтобы строить эту вебхук-логику самостоятельно, а смена статуса устроена так, чтобы опираться на то же доказательство, с фасилитатором @tam, который выходит поверх этого механизма, а не на сырой write-вызов.

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