agent-news-bot — Telegram-бот, который дёргает Claude Code как агента¶
Пилотный проект: Telegram-бот на Dart, который по команде /news <тема> не сам ищет
информацию, а поручает ресёрч Claude Code, а результат в чат присылает уже сам Claude —
через собственный MCP-инструмент. Код: platformteam-projects/agent-news.
Представь секретаря на ресепшене (бот) и исследователя за соседней дверью
(Claude Code). Ты говоришь секретарю: «принеси свежие новости про X». Секретарь
сам ничего не ищет — он стучится к исследователю и передаёт задание. Исследователь
роется в интернете и сам отчитывается тебе факсом (это и есть MCP-инструмент
send_to_telegram) — сначала пару раз коротко «ищу…», «нашёл источники…», а в
конце — сам отчёт. Секретарь только передал задание, дальше не вмешивается.
sequenceDiagram
actor U as Пользователь
participant T as Telegram
participant B as Бот-секретарь
participant C as Claude Code (исследователь)
U->>T: пишет /news <тема>
Note over B,T: бот сам стучится в Telegram и забирает<br/>сообщение (long polling) — не наоборот
B->>T: забирает сообщение
B->>T: "Принял, начинаю…"
T->>U: приходит в чат
B->>C: запускает как процесс, даёт задание
C->>C: ищет в интернете
C->>T: статус "⏳ ищу источники…" (MCP-тул)
T->>U: статус приходит в чат
C->>C: готовит выжимку
C->>T: финальный отчёт (тот же MCP-тул)
T->>U: выжимка приходит в чат
Важная деталь: пользователь пишет в Telegram, а не напрямую боту — у бота нет
и не может быть прямого канала к пользователю. Бот сам, постоянно, ходит в Telegram
Bot API и спрашивает «есть что-то новое для меня?» (это и называется long polling,
getUpdates), а не Telegram сам «стучится» к боту при получении сообщения.
Почему статусы по ходу важны? Потому что реальный ресёрч занимает не секунды, а несколько минут — без промежуточных сообщений человек решает, что бот завис, хотя он честно работает. Это не теория: именно так и получилось на первом же тесте.
Один Docker-контейнер, три процесса:
agent-news-bot(Dart, long polling Telegram API) — слушает/news <тема>, спавнитclaudeкак локальный сабпроцесс. Отдельного контейнера/пода для Claude Code нет: на связке «бот дёргает docker exec в соседний контейнер» пришлось бы либо давать боту права наpods/execв K8s, либо монтировать docker.sock — то и другое лишняя дыра ради удобства. Сабпроцесс даёт тот же эффект (переиспользуемый persistent процесс) без этой сложности.claude(Claude Code CLI, запускается с-p "<промпт>" --mcp-config mcp.json --strict-mcp-config --dangerously-skip-permissions) — делает ресёрч через встроенные тулы (веб-поиск), по ходу и в конце вызывает MCP-тулsend_to_telegram.agent-news-mcp(Dart,package:mcp_dart, транспорт stdio) — спавнится самимclaudeпоmcp.json, реализует единственный тулsend_to_telegram(chat_id,text) и шлёт сообщение в Telegram Bot API напрямую. Тул рассчитан на несколько вызовов за сессию — не только на финальный ответ.
flowchart LR
U["Пользователь"]
TG["Telegram Bot API"]
subgraph Container["один Docker-контейнер"]
Bot["agent-news-bot<br/>(Dart)"]
Claude["claude CLI<br/>(сабпроцесс, -p)"]
MCP["agent-news-mcp<br/>(stdio MCP-сервер)"]
end
U -->|"/news <тема>"| TG
Bot -->|"getUpdates, long polling —<br/>бот САМ опрашивает Telegram"| TG
Bot -->|"Process.start('claude', ...)"| Claude
Claude -->|"stdio, tools/call<br/>send_to_telegram (×N: статусы + финал)"| MCP
MCP -->|"sendMessage"| TG
TG -->|"статусы + выжимка"| U
На диаграмме два разных направления к Telegram специально нарисованы отдельно:
Bot → TG (опрос за новыми сообщениями, pull) и MCP → TG (отправка результата,
push). Прямой связи «пользователь → бот» в принципе не существует — весь трафик в
обе стороны идёт через Telegram Bot API.
Ключевые детали и грабли, на которые реально наступили при сборке:
- Авторизация Claude Code — OAuth-логин через Claude.ai (подписка, не
API-ключ). Разово:
docker compose exec agent-news claude auth login, перейти по ссылке в браузере, вставить код обратно в терминал. - Грабли №1 — не тот путь монтирования. Первая версия монтировала volume
только на
~/.claude/. Логин проходил,claude auth statusбыл честныйloggedIn: true— а после пересборки контейнера снова требовал логина. Причина: claude-code пишет учётные данные не только в~/.claude/, но и отдельным файлом~/.claude.jsonрядом с этой папкой — он оставался на эфемерном слое контейнера. Фикс — монтировать volume на весь$HOME, а статичныйmcp.jsonдержать вне home (/app/claude-config), чтобы пустой volume не затирал его при первом старте. - Грабли №2 — тишина по 3+ минуты. Даже тривиальный принудительный вызов тула
(без всякого веб-поиска) занял у Claude Code 3 минуты 43 секунды сквозного
времени. Реальный ресёрч — дольше. Без промежуточных статусов это неотличимо от
зависания. Фикс — в промпте отдельным пунктом просим Claude присылать короткие
send_to_telegram-статусы по ходу работы, а не только в конце. - Один запрос за раз —
ClaudeRunnerв боте держит простой busy-флаг: пока идёт ресёрч, новый/newsполучает «уже выполняю, подожди». --strict-mcp-config— Claude использует только MCP-сервер из явно переданногоmcp.json, не подхватывает ничего лишнего из окружения.- Тема — не хардкод.
/newsбез аргумента берётDEFAULT_TOPIC(по умолчанию просто «главные новости дня»), а/news <тема>— любую тему без ограничений по сфере. Все формулировки, которые бот показывает пользователю или подставляет в промпт Claude, вынесены в один файл —bot/lib/lexicon.dart. - Формат ответа — вся содержательная часть уходит от Claude напрямую в
Telegram через MCP-тул; сам
claude -pв stdout бот не парсит и не пересылает — это осознанный выбор: форматирование (Markdown, разбивка по сообщениям, статусы) полностью на стороне модели.
Сейчас проект гоняется только локально (docker compose up -d --build), без
деплоя в K3s — это ещё пилот.
Домашнее задание
Собери свою версию этой же схемы (бот → агент как процесс → MCP-тул →
доставка результата) под другую задачу — не новости. Например: /review <ссылка
на PR> (агент смотрит диф и комментирует), /summarize <ссылка> (агент
пересказывает документ/статью), /standup (агент собирает статусы задач и
оформляет саммари). Тема на твой выбор — важна архитектура, не сюжет.
Обязательный чек-лист (это реальные грабли из сегодняшней сборки, наступи на них осознанно, а не случайно):
- Бот на Dart (или любом языке) спавнит
claude -pкак сабпроцесс — не черезdocker execв соседний контейнер. - Свой MCP-сервер с минимум одним тулом доставки результата (
send_to_telegram,send_to_slack— что подходит под твой канал). - Volume для
$HOMEцеликом, не только для~/.claude/— иначе логин слетит при пересборке (см. «Грабли №1» выше). - Промежуточные статусы через тот же MCP-тул, а не только финальный ответ — иначе на реальном тайминге (минуты, не секунды) это выглядит как зависание.
- Все тексты бота — в отдельном lexicon-файле, не разбросаны по коду.
Готово — открой PR и приложи короткое демо (скриншот переписки в чате).
Связанные страницы¶
- mcp.md — что такое MCP и зачем он вообще нужен
- agents.md — агенты и тулы в целом
- agent-ssh-deploy.md — ещё один пример «агент сам выполняет действие», но по SSH