trello-tools: плагин и маркетплейс gp-tools для Claude Code
Сервер запускается через uv run --script, ключи запрашиваются при установке плагина (userConfig) вместо .env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["mcp>=2.1,<3", "httpx>=0.28", "python-dotenv>=1.0"]
|
||||
# ///
|
||||
"""trello-mcp — MCP-сервер (stdio) для связки Trello + qa-docs-service.
|
||||
|
||||
Запускается как обычный локальный процесс, Claude Code общается с ним
|
||||
через stdin/stdout. Регистрация (пример, scope local):
|
||||
|
||||
claude mcp add trello-tools --scope local -- \\
|
||||
~/projects/gp_mcp/trello-mcp/.venv/bin/python \\
|
||||
~/projects/gp_mcp/trello-mcp/server.py
|
||||
|
||||
Либо как плагин trello-tools из маркетплейса gp-tools (см. README.md):
|
||||
тогда сервер запускается через `uv run --script`, зависимости берутся из
|
||||
блока `# /// script` выше, а настройки — из userConfig плагина.
|
||||
|
||||
Примечание: план изначально предполагал `mcp.server.fastmcp.FastMCP`
|
||||
(SDK v1). В установленной версии `mcp` (2.x) декораторный API называется
|
||||
`MCPServer` (`mcp.server.mcpserver.MCPServer`) — по сути тот же
|
||||
декоратор `.tool()`, только с другим именем класса.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server.mcpserver import AcceptedElicitation, Context, Image, MCPServer
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import qa_client
|
||||
import trello
|
||||
|
||||
load_dotenv(Path(__file__).parent / ".env")
|
||||
|
||||
# В плагине .env нет: Claude Code передаёт поля userConfig из
|
||||
# .claude-plugin/plugin.json как CLAUDE_PLUGIN_OPTION_<KEY>. Уже заданные
|
||||
# переменные (например, QA_TASKS_TOKEN из ~/.bashrc) не перетираются.
|
||||
PLUGIN_OPTIONS = (
|
||||
"TRELLO_API_KEY",
|
||||
"TRELLO_TOKEN",
|
||||
"TRELLO_BOARD_ID",
|
||||
"TRELLO_CREATE_LISTS",
|
||||
"TRELLO_ATTACHMENTS_DIR",
|
||||
"QA_SERVICE_URL",
|
||||
"QA_SERVICE_API_KEY",
|
||||
"QA_TASKS_TOKEN",
|
||||
)
|
||||
for _name in PLUGIN_OPTIONS:
|
||||
_value = os.environ.get(f"CLAUDE_PLUGIN_OPTION_{_name}")
|
||||
if _value and not os.environ.get(_name):
|
||||
os.environ[_name] = _value
|
||||
|
||||
mcp = MCPServer("trello-tools")
|
||||
|
||||
QA_DOC_TEMPLATE = """\
|
||||
# <Заголовок задачи> — [ссылка на карточку Trello]
|
||||
|
||||
## Что сделано
|
||||
2-4 предложения по сути изменений.
|
||||
|
||||
## Было
|
||||
Как вело себя раньше, конкретно, с примером.
|
||||
|
||||
## Стало
|
||||
Как работает сейчас.
|
||||
|
||||
## Как проверить
|
||||
Каждый сценарий — отдельный заголовок третьего уровня, начинающийся со
|
||||
слова «Сценарий» (в украинском документе — «Сценарій»). Под каждым таким
|
||||
заголовком на страничке появятся галочка «Проверено», выбор «Работает /
|
||||
Не работает» и поле комментария для тестировщика (что именно не
|
||||
работает). Шаги — списком под заголовком.
|
||||
|
||||
### Сценарий 1: <короткое название>
|
||||
1. Шаг
|
||||
2. Шаг
|
||||
Ожидаемый результат: ...
|
||||
|
||||
### Сценарий 2: <короткое название>
|
||||
...
|
||||
|
||||
## Что могло зацепить
|
||||
Смежные места / риски регрессии.
|
||||
|
||||
---
|
||||
Дата, ветка/коммит — если уместно."""
|
||||
|
||||
SCENARIOS_NOTE = (
|
||||
"Отметки тестировщиков хранятся отдельно от markdown и при обновлении "
|
||||
"документа сохраняются — но привязаны к тексту заголовка сценария: "
|
||||
"если переименовать «### Сценарий ...», его отметка сбросится. "
|
||||
"Не меняй заголовки сценариев без необходимости."
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Вызывай, когда пользователь прислал ссылку на карточку Trello — "
|
||||
"чтобы подтянуть контекст задачи в разговор, включая обсуждение "
|
||||
"в комментариях.\n\n"
|
||||
"Вход: card_url — ссылка вида https://trello.com/c/abc123/45-название.\n"
|
||||
"Выход: {id, name, desc, url, board_id, list_id, labels, attachments, "
|
||||
"comments}.\n"
|
||||
"labels — метки карточки [{name, color}].\n"
|
||||
"attachments — только СПИСОК вложений, без содержимого: [{id, name, "
|
||||
"kind, mime, size, date, url}], kind — image (картинка), file (файл), "
|
||||
"card (другая карточка Trello), link (прочая ссылка). Содержимое "
|
||||
"тяни через trello_get_attachment, только если оно нужно для задачи "
|
||||
"(скриншот бага, макет, лог, ТЗ, связанная карточка) — не всё подряд.\n"
|
||||
"comments — список {author, text, date} в хронологическом порядке "
|
||||
"(старые → новые), до 20 последних.\n\n"
|
||||
"Если в описании или комментариях есть ссылка на задание вида "
|
||||
".../tasks/<slug> — это задание для фронта/бэка от другой сессии "
|
||||
"Claude: прочитай его через get_dev_task. Все задания по карточке "
|
||||
"можно найти через list_dev_tasks(card_url)."
|
||||
)
|
||||
)
|
||||
async def trello_get_card(card_url: str) -> dict:
|
||||
return await trello.get_card(card_url)
|
||||
|
||||
|
||||
# Картинки больше этого не отдаём в контекст — только путь к файлу.
|
||||
INLINE_IMAGE_MAX_BYTES = 5 * 1024 * 1024
|
||||
# Текстовые файлы до этого размера отдаём прямо в ответе.
|
||||
INLINE_TEXT_MAX_BYTES = 200 * 1024
|
||||
_TEXT_MIMES = ("application/json", "application/xml", "application/x-yaml", "application/csv")
|
||||
# Форматы, которые модель умеет смотреть как картинку; прочие (svg, heic…) — по path.
|
||||
_INLINE_IMAGE_MIMES = ("image/png", "image/jpeg", "image/gif", "image/webp")
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Достаёт одно вложение карточки Trello. Вызывай выборочно — когда "
|
||||
"вложение из attachments в trello_get_card действительно нужно для "
|
||||
"задачи.\n\n"
|
||||
"Вход: card_id — id карточки (поле id из trello_get_card), "
|
||||
"attachment_id — id вложения из attachments.\n"
|
||||
"Что вернётся по kind:\n"
|
||||
"- image — метаданные + сама картинка (png/jpeg/gif/webp до 5 МБ; "
|
||||
"иначе — только path);\n"
|
||||
"- file — метаданные + path к скачанному файлу; текстовые файлы до "
|
||||
"200 КБ — ещё и text. Остальное (PDF, docx, архивы) читай по path "
|
||||
"своими инструментами;\n"
|
||||
"- card — связанная карточка целиком, как из trello_get_card;\n"
|
||||
"- link — только метаданные и url, сам сайт не открывается.\n"
|
||||
"Файлы > 25 МБ не скачиваются."
|
||||
)
|
||||
)
|
||||
async def trello_get_attachment(card_id: str, attachment_id: str):
|
||||
attachment = await trello.get_attachment(card_id, attachment_id)
|
||||
|
||||
if attachment["kind"] == "card":
|
||||
return {**attachment, "card": await trello.get_card(attachment["url"])}
|
||||
if attachment["kind"] == "link":
|
||||
return attachment
|
||||
|
||||
data = attachment.pop("data")
|
||||
mime = attachment["mime"] or ""
|
||||
|
||||
if mime in _INLINE_IMAGE_MIMES and len(data) <= INLINE_IMAGE_MAX_BYTES:
|
||||
return [attachment, Image(data=data, format=mime.split("/", 1)[1])]
|
||||
|
||||
if (mime.startswith("text/") or mime in _TEXT_MIMES) and len(data) <= INLINE_TEXT_MAX_BYTES:
|
||||
try:
|
||||
attachment["text"] = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return attachment
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Постит текстовый комментарий в карточку Trello — любой текст, "
|
||||
"ссылка не обязательна.\n\n"
|
||||
"Произвольные комментарии — ТОЛЬКО по прямой просьбе пользователя "
|
||||
"(«напиши в карточку…», «оставь комментарий…»). Текст бери "
|
||||
"дословно из просьбы; если сформулировать нужно тебе — сначала "
|
||||
"покажи текст и дождись одобрения. Исключение — комментарий со "
|
||||
"ссылкой после publish_qa_doc / publish_dev_task: его правила "
|
||||
"описаны в этих инструментах.\n\n"
|
||||
"Вход: card_id — полный id карточки (не shortLink; его отдаёт "
|
||||
"trello_get_card в поле id), text — текст комментария.\n"
|
||||
"Выход: {id} — id созданного комментария."
|
||||
)
|
||||
)
|
||||
async def trello_add_comment(card_id: str, text: str) -> dict:
|
||||
return await trello.add_comment(card_id, text)
|
||||
|
||||
|
||||
class CardConfirmation(BaseModel):
|
||||
# Включена по умолчанию: подтверждение — это нажатие «принять», вторая
|
||||
# ступень в том же окне только путает (галочку не замечают, и карточка
|
||||
# молча не создаётся). Снять её — тоже способ отказаться.
|
||||
confirm: bool = Field(
|
||||
default=True, description="Создать карточку именно с этим текстом"
|
||||
)
|
||||
|
||||
|
||||
def _card_preview(
|
||||
list_name: str,
|
||||
name: str,
|
||||
desc: str,
|
||||
template: dict | None = None,
|
||||
labels: list[dict] | None = None,
|
||||
) -> str:
|
||||
lines = [f"Столбец: {list_name}"]
|
||||
if template:
|
||||
lines.append(f"Шаблон: {template['name']}")
|
||||
if template["members"]:
|
||||
lines.append(f" участники: {', '.join(template['members'])}")
|
||||
if template["cover"]:
|
||||
lines.append(f" обложка: {template['cover']['color']}")
|
||||
if template["labels"]:
|
||||
lines.append(f" метки шаблона: {', '.join(l['name'] or l['color'] for l in template['labels'])}")
|
||||
lines += [f" чек-лист «{c['name']}»" for c in template["checklists"]]
|
||||
if labels:
|
||||
lines.append(f"Метки: {', '.join(label['name'] for label in labels)}")
|
||||
return (
|
||||
"Создать карточку в Trello?\n\n"
|
||||
+ "\n".join(lines)
|
||||
+ f"\nНазвание: {name}\n\n"
|
||||
f"Описание:\n{desc or '(пусто)'}"
|
||||
)
|
||||
|
||||
|
||||
async def create_card_confirmed(
|
||||
ctx: Context,
|
||||
list_name: str,
|
||||
name: str,
|
||||
desc: str,
|
||||
template_name: str | None = None,
|
||||
label_names: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Создаёт карточку только после подтверждения пользователем в форме клиента.
|
||||
|
||||
Подтверждение запрашивается у пользователя напрямую (MCP elicitation),
|
||||
в обход модели: карточка создаётся ровно с тем текстом, который он видел.
|
||||
"""
|
||||
target = await trello.resolve_create_list(list_name)
|
||||
template = await trello.resolve_template(template_name) if template_name else None
|
||||
labels = await trello.resolve_labels(label_names) if label_names else []
|
||||
|
||||
try:
|
||||
answer = await ctx.elicit(
|
||||
_card_preview(target["name"], name, desc, template, labels), CardConfirmation
|
||||
)
|
||||
except MCPError as error:
|
||||
return {
|
||||
"created": False,
|
||||
"reason": (
|
||||
"Клиент не смог показать пользователю окно подтверждения "
|
||||
f"({error}). Карточка НЕ создана."
|
||||
),
|
||||
}
|
||||
|
||||
if not isinstance(answer, AcceptedElicitation):
|
||||
return {
|
||||
"created": False,
|
||||
"reason": "Пользователь отклонил или закрыл окно подтверждения. Карточка НЕ создана.",
|
||||
}
|
||||
if not answer.data.confirm:
|
||||
return {
|
||||
"created": False,
|
||||
"reason": (
|
||||
"Пользователь принял окно, но снял галочку «Создать карточку». "
|
||||
"Карточка НЕ создана."
|
||||
),
|
||||
}
|
||||
|
||||
card = await trello.create_card(
|
||||
target["id"], name, desc, template, [label["id"] for label in labels]
|
||||
)
|
||||
return {
|
||||
"created": True,
|
||||
"list": target["name"],
|
||||
"template": template["name"] if template else None,
|
||||
"labels": [label["name"] for label in labels],
|
||||
**card,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Список шаблонов карточек на доске Trello. Шаблон задаёт участников "
|
||||
"карточки и оформление (обложку, метки, чек-листы) — по названию "
|
||||
"видно, для какого типа задач он (баг, пожелание клиента, фронт…). "
|
||||
"Вызывай перед trello_create_card. Только чтение — можно без спроса.\n\n"
|
||||
"Выход: [{id, name, list, desc, members, cover, labels, checklists}]. "
|
||||
"Всё это переносится в новую карточку автоматически; desc обычно "
|
||||
"пуст, если нет — повтори его структуру в описании."
|
||||
)
|
||||
)
|
||||
async def trello_list_card_templates() -> list[dict]:
|
||||
return await trello.list_templates()
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Список меток доски Trello [{id, name, color, uses}], самые "
|
||||
"используемые первыми (uses — на скольких карточках стоит). Вызывай "
|
||||
"перед trello_create_card, чтобы подобрать метки. Только чтение — "
|
||||
"можно без спроса."
|
||||
)
|
||||
)
|
||||
async def trello_list_labels() -> list[dict]:
|
||||
return await trello.list_labels()
|
||||
|
||||
|
||||
_create_targets = trello.create_targets()
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Создаёт карточку в Trello. ВЫЗЫВАЙ ТОЛЬКО по прямой просьбе "
|
||||
"пользователя создать карточку («создай карточку», «заведи задачу "
|
||||
"в Trello» и т.п.). Не создавай карточки по своей инициативе — "
|
||||
"даже если по ходу работы нашёл баг или это кажется полезным; "
|
||||
"в таком случае максимум предложи.\n\n"
|
||||
"Порядок:\n"
|
||||
"1. Вызови trello_list_card_templates и выбери подходящий шаблон "
|
||||
"(баг → «баг», пожелание клиента → «Пожелание клиента» и т.п.); если "
|
||||
"пользователь назвал шаблон — бери его, если неочевидно — спроси. "
|
||||
"Составь черновик: столбец, шаблон, метки, название, описание (markdown). "
|
||||
"Если у шаблона есть desc — повтори его структуру (те же заголовки "
|
||||
"и порядок) и заполни. Метки и чек-листы шаблона перенесутся сами — "
|
||||
"в описании их не дублируй.\n"
|
||||
" Подбери метки через trello_list_labels:\n"
|
||||
" - по смыслу — тематические: раздел/модуль продукта, интеграция, "
|
||||
"тип задачи (bug…), приоритет (Minor/Major/Critical…); ставь только "
|
||||
"уверенно подходящие, лучше меньше, чем лишние;\n"
|
||||
" - ТОЛЬКО по указанию пользователя — метки с именами людей и "
|
||||
"статусные/процессные (Доробити, READY TO FRONT, NEED_HOTFIX, "
|
||||
"WAITING…, Перевірити на проді и т.п.);\n"
|
||||
" - метки, которые назвал пользователь, ставь всегда.\n"
|
||||
" Метки шаблона добавятся сами, их в labels не повторяй.\n"
|
||||
"2. Покажи черновик пользователю в чате ДОСЛОВНО и спроси, "
|
||||
"создавать ли. Правки пользователя вноси и показывай снова.\n"
|
||||
"3. Вызывай тул только после явного «да» и передавай ровно "
|
||||
"одобренный текст, ничего не меняя.\n"
|
||||
"Тул дополнительно сам покажет пользователю окно подтверждения; "
|
||||
"если он откажет — карточка не создастся (created=false), "
|
||||
"не пытайся вызвать повторно без новой просьбы.\n\n"
|
||||
"Разрешённые столбцы (list_name — строго одно из): "
|
||||
+ (
|
||||
", ".join(f"«{name}»" for name in _create_targets)
|
||||
if _create_targets
|
||||
else "НЕ НАСТРОЕНЫ (TRELLO_CREATE_LISTS пуст) — создание недоступно"
|
||||
)
|
||||
+ ". Если пользователь не указал столбец и вариантов несколько — "
|
||||
"спроси.\n\n"
|
||||
"Вход: list_name, name — название карточки, desc — описание, "
|
||||
"template — название шаблона из trello_list_card_templates "
|
||||
"(опционально, без шаблона — пустая карточка), labels — названия "
|
||||
"меток из trello_list_labels (опционально).\n"
|
||||
"Выход: {created, reason} при отказе или {created, list, template, "
|
||||
"labels, id, name, url}."
|
||||
)
|
||||
)
|
||||
async def trello_create_card(
|
||||
list_name: str,
|
||||
name: str,
|
||||
desc: str,
|
||||
ctx: Context,
|
||||
template: str | None = None,
|
||||
labels: list[str] | None = None,
|
||||
) -> dict:
|
||||
return await create_card_confirmed(ctx, list_name, name, desc, template, labels)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Публикует НОВЫЙ QA-документ по итогам работы и отдаёт стабильную "
|
||||
"ссылку. Если для этой же карточки Trello в этом разговоре уже "
|
||||
"публиковался QA-документ и известен его slug (из url вида "
|
||||
".../docs/<slug>) — используй вместо этого update_qa_doc, чтобы "
|
||||
"обновить существующую ссылку, а не плодить новую при каждом "
|
||||
"повторном прогоне. Если документ не привязан к карточке Trello — "
|
||||
"используй publish_standalone_qa_doc.\n\n"
|
||||
"markdown должен следовать шаблону:\n\n"
|
||||
f"{QA_DOC_TEMPLATE}\n\n"
|
||||
"После публикации, если известен card_id карточки Trello, вызови "
|
||||
'trello_add_comment(card_id, f"QA: {url}") с URL из ответа этого тула, '
|
||||
"чтобы тестировщик увидел ссылку в комментарии к карточке.\n\n"
|
||||
"Вход: title, markdown (по шаблону выше), card_url — опционально, "
|
||||
"ссылка на исходную карточку Trello для шапки странички.\n"
|
||||
"Выход: {slug, url}."
|
||||
)
|
||||
)
|
||||
async def publish_qa_doc(
|
||||
title: str, markdown: str, card_url: str | None = None
|
||||
) -> dict:
|
||||
return await qa_client.publish_doc(title, markdown, card_url)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Обновляет УЖЕ ОПУБЛИКОВАННЫЙ QA-документ на месте — тот же slug, "
|
||||
"та же ссылка, тестировщику не нужно переходить по новой. Используй "
|
||||
"вместо publish_qa_doc, когда повторно дорабатываешь ту же карточку "
|
||||
"Trello в рамках этого разговора и знаешь slug из предыдущего "
|
||||
"publish_qa_doc/update_qa_doc (это последний сегмент url — "
|
||||
".../docs/<slug>).\n\n"
|
||||
"markdown — по тому же шаблону, что и в publish_qa_doc:\n\n"
|
||||
f"{QA_DOC_TEMPLATE}\n\n"
|
||||
"Если slug не найден на сервере — вернётся ошибка 404; в этом "
|
||||
"случае используй publish_qa_doc, чтобы опубликовать заново.\n\n"
|
||||
"Чтобы поправить документ, которого нет в текущем разговоре, сначала "
|
||||
"скачай его через get_qa_doc. Поля перезаписываются целиком: передай "
|
||||
"card_url и problem заново (их отдаёт get_qa_doc), иначе они "
|
||||
f"очистятся. {SCENARIOS_NOTE}\n\n"
|
||||
"Вход: slug (или полная ссылка), title, markdown (по шаблону выше), "
|
||||
"card_url и problem — опционально.\n"
|
||||
"Выход: {slug, url}."
|
||||
)
|
||||
)
|
||||
async def update_qa_doc(
|
||||
slug: str,
|
||||
title: str,
|
||||
markdown: str,
|
||||
card_url: str | None = None,
|
||||
problem: str | None = None,
|
||||
) -> dict:
|
||||
return await qa_client.update_doc(slug, title, markdown, card_url, problem)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Публикует документ проверки, НЕ привязанный к карточке Trello, — "
|
||||
"например, когда пользователь просит выгрузить проверку по проблеме, "
|
||||
"для которой карточки нет. Описание проблемы выводится отдельным "
|
||||
"блоком в шапке странички, поэтому в markdown его не дублируй.\n\n"
|
||||
"markdown — по тому же шаблону, что и в publish_qa_doc, только без "
|
||||
"ссылки на карточку в заголовке:\n\n"
|
||||
f"{QA_DOC_TEMPLATE}\n\n"
|
||||
"Обновлять такой документ — через update_qa_doc, передавая problem "
|
||||
"заново.\n\n"
|
||||
"Вход: title, problem — описание проблемы (что не так, где, как "
|
||||
"воспроизводится; можно markdown), markdown (по шаблону выше).\n"
|
||||
"Выход: {slug, url}."
|
||||
)
|
||||
)
|
||||
async def publish_standalone_qa_doc(title: str, problem: str, markdown: str) -> dict:
|
||||
return await qa_client.publish_doc(title, markdown, card_url=None, problem=problem)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Скачивает уже опубликованный QA-документ: исходный markdown, "
|
||||
"метаданные и отметки тестировщиков по каждому сценарию.\n\n"
|
||||
"Используй, когда нужно:\n"
|
||||
"- поправить существующий документ — скачай, отредактируй markdown "
|
||||
"и отправь через update_qa_doc с тем же slug (ссылка не изменится);\n"
|
||||
"- узнать результаты проверки — какие сценарии тестировщик проверил, "
|
||||
"какие работают, какие нет и что он написал в комментариях.\n\n"
|
||||
"Вход: slug или полная ссылка на документ (.../docs/<slug>).\n"
|
||||
"Выход: {slug, url, title, markdown, card_url, problem, created_at, "
|
||||
"updated_at, scenarios}. scenarios — список в порядке документа: "
|
||||
"{scenario — заголовок, checked — проверен ли, passed — true "
|
||||
"работает / false не работает / null не проверен, comment — "
|
||||
"комментарий тестировщика (что именно не работает), updated_at}."
|
||||
)
|
||||
)
|
||||
async def get_qa_doc(slug: str) -> dict:
|
||||
return await qa_client.get_doc(slug)
|
||||
|
||||
|
||||
TASK_TARGETS = "front | back"
|
||||
|
||||
TASK_GUIDE = (
|
||||
"markdown — в свободной форме, но пишется для ДРУГОЙ нейронки, у которой "
|
||||
"нет этого разговора и, возможно, нет доступа к этому репозиторию. "
|
||||
"Поэтому явно: контекст задачи и зачем это нужно; что именно сделать; "
|
||||
"контракт на стыке (эндпоинты, методы, поля запроса/ответа с типами и "
|
||||
"примерами, коды ошибок); что уже сделано на нашей стороне и где "
|
||||
"(ветка/файлы); открытые вопросы. Заголовок в markdown не повторяй — "
|
||||
"он берётся из title."
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Публикует задание для фронта или бэка — документ для другой "
|
||||
"нейронки (Claude на той стороне), которая будет его делать.\n\n"
|
||||
"Когда: по ходу работы над карточкой Trello понял, что нужна "
|
||||
"работа на другой стороне (например, бэку нужен эндпоинт от фронта "
|
||||
"или наоборот). ПРЕДЛОЖИ пользователю одной фразой: что за задание "
|
||||
"и кому, и что оставишь ссылку в карточке. Если согласился (даже "
|
||||
"коротко: «да», «давай», «ок») — публикуй сразу, дословно текст "
|
||||
"на одобрение показывать не нужно. Без согласия не публикуй.\n\n"
|
||||
f"{TASK_GUIDE}\n\n"
|
||||
"После публикации, если известен card_id карточки, оставь "
|
||||
"комментарий через trello_add_comment — ровно три строки:\n"
|
||||
"Задание для <фронта|бэка>: <title>\n<url>\n"
|
||||
'Получить: curl -H "Authorization: Bearer $QA_TASKS_TOKEN" <url>\n\n'
|
||||
"Если по этой же карточке задание для той же стороны уже "
|
||||
"публиковалось (slug известен или находится через list_dev_tasks) — "
|
||||
"обнови его через update_dev_task, а не плоди новое.\n\n"
|
||||
f"Вход: target ({TASK_TARGETS}) — кому задание, title, markdown, "
|
||||
"card_url — ссылка на карточку Trello, если есть.\n"
|
||||
"Выход: {slug, url, title, target, card_url, created_at, updated_at}."
|
||||
)
|
||||
)
|
||||
async def publish_dev_task(
|
||||
target: Literal["front", "back"],
|
||||
title: str,
|
||||
markdown: str,
|
||||
card_url: str | None = None,
|
||||
) -> dict:
|
||||
return await qa_client.publish_task(target, title, markdown, card_url)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Обновляет уже опубликованное задание для фронта/бэка на месте — "
|
||||
"ссылка не меняется. Правила согласия те же, что у "
|
||||
"publish_dev_task: предложил — пользователь согласился — обновляешь. "
|
||||
"Если текущего текста нет в разговоре, сначала скачай его через "
|
||||
"get_dev_task. Поля перезаписываются целиком.\n\n"
|
||||
f"{TASK_GUIDE}\n\n"
|
||||
f"Вход: slug (или ссылка), target ({TASK_TARGETS}), title, markdown, "
|
||||
"card_url.\nВыход: как у publish_dev_task."
|
||||
)
|
||||
)
|
||||
async def update_dev_task(
|
||||
slug: str,
|
||||
target: Literal["front", "back"],
|
||||
title: str,
|
||||
markdown: str,
|
||||
card_url: str | None = None,
|
||||
) -> dict:
|
||||
return await qa_client.update_task(slug, target, title, markdown, card_url)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Читает задание для фронта/бэка по slug или ссылке вида "
|
||||
".../tasks/<slug> — например, из комментария в карточке Trello. "
|
||||
"Вызывай без спроса, это только чтение.\n\n"
|
||||
"Выход: {slug, url, title, target, card_url, markdown, created_at, "
|
||||
"updated_at}."
|
||||
)
|
||||
)
|
||||
async def get_dev_task(slug: str) -> dict:
|
||||
return await qa_client.get_task(slug)
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Список заданий для фронта/бэка (без текста, новые первыми). "
|
||||
"Используй, когда пользователь прислал карточку Trello — чтобы "
|
||||
"проверить, нет ли по ней заданий для твоей стороны; или чтобы "
|
||||
"найти slug уже опубликованного задания перед обновлением. "
|
||||
"Вызывай без спроса, это только чтение.\n\n"
|
||||
f"Вход: card_url — опционально, target ({TASK_TARGETS}) — опционально.\n"
|
||||
"Выход: [{slug, url, title, target, card_url, created_at, updated_at}]."
|
||||
)
|
||||
)
|
||||
async def list_dev_tasks(
|
||||
card_url: str | None = None, target: Literal["front", "back"] | None = None
|
||||
) -> list[dict]:
|
||||
return await qa_client.list_tasks(card_url, target)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
Reference in New Issue
Block a user