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

Если ваш трекер не поставляет официальный MCP-сервер, а официальный дает только доступ на чтение, вы в итоге строите свой. Задача меньше, чем кажется. Проблема с безопасностью куда больше, чем кажется: в момент, когда ИИ-агент получает возможность вызвать create_issue или delete_task против настоящей доски, вы построили не скрипт-интеграцию, а инфраструктуру контроля доступа.
Дальше разберём реальный процесс сборки: как замапить API шести трекеров на инструменты MCP, где у каждого из них модель авторизации незаметно кусается, и какие решения по разграничению доступа определяют, сможет ли агент делать ровно то, что должен, и ничего сверх этого.
Что на самом деле даёт MCP
Model Context Protocol предоставляет ИИ-клиенту два вида сущностей: инструменты (функции, которые модель может вызвать, например create_issue) и ресурсы (данные, которые она может прочитать, например список задач проекта). Работает это через один из двух транспортов:
- stdio: локальный подпроцесс, который клиент запускает сам. Подходит для машины одного разработчика.
- Streamable HTTP: сервер, к которому подключаются удалённо другие люди и другие агенты. Именно здесь аутентификация становится вашей проблемой, а не проблемой трекера.
Всё, что дальше, рассчитано на то, что в итоге вы выкатите именно HTTP-версию: ей пользуется вся команда, а не только вы.
Шаг 1: сопоставить API и модель авторизации каждого трекера
У каждого трекера ниже есть рабочий REST или GraphQL API. Различаются они моделью авторизации, и именно здесь наивная интеграция обычно ломается первой.
| Трекер | Форма API | Авторизация | Подводный камень |
|---|---|---|---|
| Jira | REST v3 | API-токен + email, либо OAuth 2.0 (3LO) для мульти-тенантных приложений | Персональный API-токен даёт доступ ко всему, что видит аккаунт, а не к одному проекту |
| Linear | GraphQL | Персональный API-ключ или OAuth2 | Персональный ключ наследует все права создателя в воркспейсе |
| Asana | REST | Персональный токен доступа (PAT) или OAuth2 | PAT не истекает сам по себе; утёкший токен остаётся рабочим, пока кто-то не вспомнит его отозвать |
| ClickUp | REST v2 | Персональный токен или OAuth2 | Скоуп токена по умолчанию: весь воркспейс |
| Trello | REST | Пара API key + token | Токен генерируется на пользователя по всем доскам, которые он видит, а не под конкретную доску |
| Monday.com | GraphQL | Персональный 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 на рейт-лимиты. Посмотрите полный разбор настройки и попробуйте на своём воркспейсе.