Синдром пустой ленты: почему ваш Telegram-канал умирает
Каждое утро вы открываете Telegram-канал. 342 подписчика. Последний пост — три дня назад. Руки не доходят, идеи кончились, а если и появляются — на написание уходит час. Знакомо? Я прошел через это дважды. Первый раз — когда вел технический блог и сдох через две недели. Второй — когда написал MCP-сервер, который делает всё сам.
Секрет не в магии, а в связке: Claude + Model Context Protocol (MCP). Claude умеет генерировать контент. MCP — давать ему инструменты для работы с реальным миром. Научите Claude запускать поиск на Civitai, скачивать изображения, писать посты и отправлять их в Telegram — и канал оживет без вас.
Мы уже говорили о том, как MCP превращает Claude Code из простого генератора в инженера, и как MCP Chat Studio v2 упрощает отладку. Теперь перейдем к делу: напишем 4 MCP-сервера своими руками. Никаких готовых npm-пакетов — только код, который вы поймете и сможете изменить под себя.
Весь код — на Python 3.12+. Используем официальный SDK mcp версии 1.3.0 (май 2026). Архитектура асинхронная, с использованием asyncio и httpx для HTTP-запросов.
Куда мы копаем: архитектура из 4 серверов
Представьте конвейер. Каждый MCP-сервер — один станок. Claude — оператор, который решает, что и когда запускать.
- Server 1: Civitai Searcher — ищет модели и лоры по запросу через API Civitai. Возвращает URL, метаданные, теги.
- Server 2: Image Fetcher — скачивает изображения по URL, сохраняет локально, отдает пути к файлам.
- Server 3: Post Generator — принимает описание, генерирует пост (текст + описание картинки) и возвращает готовый HTML/Markdown для Telegram.
- Server 4: Telegram Publisher — отправляет пост в канал через Bot API. Поддерживает кнопки, цитаты, треды.
Все серверы независимы, но Claude может вызывать их последовательно: найди картинку → скачай → сгенерируй описание → опубликуй. Всё одной фразой.
Шаг 0: Скелет MCP-сервера — то, с чего всё начинается
Перед тем как писать конкретные серверы, разберем каркас. Он одинаков для всех.
#!/usr/bin/env python3
"""Базовый MCP-сервер. Шаблон для всех интеграций."""
from mcp.server import Server
from mcp.server.stdio import stdio_server
app = Server("my-server")
@app.tool()
async def my_tool(param: str) -> dict:
"""Описание для Claude — что делает инструмент."""
return {"result": param}
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(app))
Ключевой момент: каждый инструмент — это асинхронная функция с аннотациями типов. Claude читает docstring и типы аргументов, чтобы понять, как вызывать. Не экономьте на docstring. Пишите так, будто объясняете коллеге-джуниору. Чем точнее описание — тем реже Claude ошибается.
⚠️ Ошибка новичка: возвращать сырые данные вроде "200 OK". Claude не понимает, что это значит. Возвращайте структурированные словари с полями status, data, error.
Теперь натянем этот скелет на наши 4 сервера.
Server 1: Civitai Searcher — ищем то, что вдохновит
Civitai — кладезь моделей для Stable Diffusion. Но вручную перебирать 100 страниц? Увольте. Напишем инструмент, который по тегу возвращает топ-5 моделей с рейтингом и ссылками.
import httpx
from typing import Optional
CIVITAI_API = "https://civitai.com/api/v1/models"
@app.tool()
async def search_civitai(
query: str,
nsfw: bool = False,
limit: int = 5
) -> list[dict]:
"""
Ищет модели на Civitai по текстовому запросу.
Возвращает список моделей с названием, типом, рейтингом, количеством лайков и ссылкой.
nsfw: False — только SFW, True — всё подряд.
"""
params = {
"query": query,
"nsfw": "true" if nsfw else "false",
"limit": min(limit, 20),
"sort": "Most Downloaded"
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(CIVITAI_API, params=params, timeout=15.0)
resp.raise_for_status()
data = resp.json()
items = data.get("items", [])
results = []
for item in items[:limit]:
results.append({
"name": item["name"],
"type": item.get("type", "unknown"),
"rating": item.get("stats", {}).get("rating", 0),
"likes": item.get("stats", {}).get("likedCount", 0),
"url": f"https://civitai.com/models/{item['id']}",
"preview": item.get("modelVersions", [{}])[0].get("images", [{}])[0].get("url", "")
})
return {"status": "success", "data": results}
except httpx.HTTPStatusError as e:
return {"status": "error", "error": f"Civitai вернул {e.response.status_code}: {e.response.text[:200]}"}
except Exception as e:
return {"status": "error", "error": str(e)}
Теперь Claude может сказать: “Найди модели Cyberpunk по тэгу, только SFW” — и получит структурированный список. Обратите внимание: мы не тащим все 500 результатов. Claude перегружается от большого количества данных. limit=5 — золотая середина.
Server 2: Image Fetcher — скачиваем и готовим к посту
Civitai вернул URL картинки. Но Claude не может просто взять URL — ему нужен файл, чтобы описать его. Наш второй сервер скачивает изображение и сохраняет его локально. Claude потом прочитает файл (через базовый MCP-сервер filesystem) и напишет описание.
import os
import uuid
from pathlib import Path
DOWNLOAD_DIR = Path("/tmp/mcp_images")
DOWNLOAD_DIR.mkdir(exist_ok=True)
@app.tool()
async def fetch_image(url: str, filename: Optional[str] = None) -> dict:
"""
Скачивает изображение по URL в локальную папку.
Возвращает путь к файлу и размер в байтах.
filename — опционально, иначе генерируется UUID.
"""
if not filename:
filename = f"{uuid.uuid4().hex}.jpg"
filepath = DOWNLOAD_DIR / filename
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, timeout=30.0)
resp.raise_for_status()
filepath.write_bytes(resp.content)
return {
"status": "success",
"path": str(filepath.absolute()),
"size_bytes": len(resp.content),
"filename": filename
}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.tool()
async def clean_images(older_than_hours: int = 24) -> dict:
"""Удаляет скачанные изображения старше N часов. Экономит место."""
import time
now = time.time()
cutoff = now - older_than_hours * 3600
deleted = 0
for f in DOWNLOAD_DIR.glob("*"):
if f.stat().st_mtime < cutoff:
f.unlink()
deleted += 1
return {"status": "success", "deleted": deleted}
Добавили clean_images — чтобы не забивать диск. Claude сам решит, когда вызывать clean: можно вписать в конец пайплайна или по расписанию (но это уже задача для orchestrator'а, о чем мы писали в статье MCP Orchestrator).
Server 3: Post Generator — где Claude раскрывается
Третий сервер — хитрый. Он не генерирует пост сам (это делает Claude). Он только подготавливает шаблон. Зачем? Чтобы Claude не отвлекался на форматирование. Даем ему структуру, он заполняет.
from datetime import datetime
@app.tool()
async def generate_post_template(
image_path: str,
description_hint: str | None = None
) -> dict:
"""
Создает шаблон поста для Telegram на основе загруженного изображения.
Возвращает структуру: заголовок, описание, теги, дата.
Claude должен заполнить поля на основе контента изображения.
"""
return {
"status": "ready",
"template": {
"title": "[Название модели]",
"description": description_hint or "[Опишите, что на картинке, технику, стиль]",
"tags": "#ai #stablediffusion #civitai",
"image_path": image_path,
"scheduled_date": datetime.now().isoformat()
},
"instructions": "Заполни title и description на основе анализа изображения. Используй максимум 200 символов для description."
}
Теперь Claude видит шаблон, анализирует картинку (через filesystem-сервер, который мы настраивали в Claude Code: полное руководство) и заполняет поля. Результат — готовый пост.
Server 4: Telegram Publisher — финальный выстрел
Самый ответственный. Отправка поста в канал. Нужен bot token, chat_id (можно username). Используем официальное Python-асинхронное пакет python-telegram-bot версии 20.8+.
import os
from telegram import Bot
from telegram.error import TelegramError
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
@app.tool()
async def publish_post(
chat_id: str,
text: str,
image_path: str | None = None,
parse_mode: str = "HTML",
disable_notification: bool = False
) -> dict:
"""
Отправляет пост в Telegram-канал.
chat_id: username канала (например @my_channel) или числовой ID.
text: текст поста в HTML-формате (поддерживает , , , ).
image_path: абсолютный путь к изображению (если есть).
parse_mode: "HTML" или "Markdown".
disable_notification: True — без звука.
"""
bot = Bot(token=BOT_TOKEN)
try:
if image_path:
with open(image_path, "rb") as photo:
msg = await bot.send_photo(
chat_id=chat_id,
photo=photo,
caption=text,
parse_mode=parse_mode,
disable_notification=disable_notification
)
else:
msg = await bot.send_message(
chat_id=chat_id,
text=text,
parse_mode=parse_mode,
disable_notification=disable_notification
)
return {
"status": "success",
"message_id": msg.message_id,
"date": msg.date.isoformat(),
"chat": chat_id
}
except TelegramError as e:
return {"status": "error", "error": str(e)}
💡 Чтобы получить bot token, создайте бота через @BotFather. Chat_id для канала: добавьте бота в администраторы и отправьте любое сообщение, затем https://api.telegram.org/bot<TOKEN>/getUpdates — в ответе увидите chat.id.
Если хотите глубже разобраться в создании ботов, рекомендую курс Создание Telegram-бота и продвижение в мессенджерах от Skillbox — там и архитектура, и деплой.
Собираем всё в кучу: конфиг для Claude Desktop
Теперь соединяем серверы. Каждый запускается отдельным процессом. Claude подключается к ним через JSON-конфиг. Убедитесь, что все зависимости установлены (mcp[cli], httpx, python-telegram-bot).
{
"mcpServers": {
"civitai": {
"command": "python",
"args": ["/path/to/civitai_server.py"],
"env": {}
},
"image-fetcher": {
"command": "python",
"args": ["/path/to/image_server.py"],
"env": {}
},
"post-generator": {
"command": "python",
"args": ["/path/to/post_server.py"],
"env": {}
},
"telegram": {
"command": "python",
"args": ["/path/to/telegram_server.py"],
"env": {
"TELEGRAM_BOT_TOKEN": "ваш_токен"
}
}
}
}
Готово. Claude видит 4 сервера, каждый с инструментами. Можно писать промпты в стиле: “Найди на Civitai модели по тегу ‘retrowave’, скачай самую популярную, сгенерируй пост и опубликуй в @my_channel”. Claude сам выстроит цепочку вызовов.
Подводные камни, которые я выжег своей шкурой
- Таймауты. Claude ждет ответа от сервера не больше 30 секунд по умолчанию. Civitai иногда тупит. Увеличьте таймаут в коде сервера до 60 с, а в config добавьте
"timeout": 60000(в миллисекундах). - Токены в логах. Никогда не пишите bot token в коде. Используйте переменные окружения. Claude Code 2.0 умеет подхватывать секреты из
.env, но я все равно рекомендую явно не показывать. - Размер файлов. Telegram не принимает фото больше 10 MB. Перед отправкой сжимайте или отдавайте ошибку.
- Rate limiting. Civitai без авторизации — 100 запросов в минуту. Для продакшна добавьте API key в заголовки. Telegram Bot API — 30 сообщений/с на один чат, не дрочите.
Помните: MCP-серверы — это не магия, а код. Чем проще и предсказуемее ваш инструмент, тем реже Claude ошибается. Я намеренно не делал супер-умных серверов. Каждый делает ровно одну вещь, и делает её хорошо.
Что дальше: как не остановиться на этом
Вы написали 4 сервера. Канал автоматизирован. Но это только начало. Вот 3 идеи для следующего шага:
- Добавьте расписание. Используйте cron или MCP Orchestrator, который мы разбирали, чтобы посты выходили каждый день в 12:00.
- Подключите аналитику. Напишите сервер, который стягивает статистику из Telegram (просмотры, реакции) и отдает Claude для анализа — какие темы заходят лучше.
- Научите Claude реагировать на комментарии. MCP-сервер может читать последние сообщения канала и генерировать ответы. Только осторожно: модерацию никто не отменял.
Я потратил на разработку этой системы 4 вечера. Первый сервер — 3 часа (кривые руки). Остальные — по 40 минут. Сейчас канал ведется полностью автономно: Claude находит контент, обрабатывает, публикует. Я только слежу за метриками раз в неделю. И честно — иногда забываю, что канал мой.
“Автоматизация — это когда ты настраиваешь систему один раз, а она работает, пока ты спишь. MCP-серверы — тот самый случай.”
Если захотите добавить свой сервер или пофиксить баг — код на GitHub (ссылка в моем профиле). И не забудьте прочитать про LM Studio MCP — там альтернативный подход без облачных моделей.