MCP-серверы своими руками: 4 интеграции для Telegram + Claude | AiManual
AiManual Logo Ai / Manual.
10 Май 2026 Гайд

MCP-серверы своими руками: как написать 4 интеграции и автоматизировать Telegram-каналы с Claude

Напишите MCP-серверы с нуля: парсинг картинок, генерация постов, отправка в Telegram. Полный гайд с кодом на Python.

Синдром пустой ленты: почему ваш 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 идеи для следующего шага:

  1. Добавьте расписание. Используйте cron или MCP Orchestrator, который мы разбирали, чтобы посты выходили каждый день в 12:00.
  2. Подключите аналитику. Напишите сервер, который стягивает статистику из Telegram (просмотры, реакции) и отдает Claude для анализа — какие темы заходят лучше.
  3. Научите Claude реагировать на комментарии. MCP-сервер может читать последние сообщения канала и генерировать ответы. Только осторожно: модерацию никто не отменял.

Я потратил на разработку этой системы 4 вечера. Первый сервер — 3 часа (кривые руки). Остальные — по 40 минут. Сейчас канал ведется полностью автономно: Claude находит контент, обрабатывает, публикует. Я только слежу за метриками раз в неделю. И честно — иногда забываю, что канал мой.

“Автоматизация — это когда ты настраиваешь систему один раз, а она работает, пока ты спишь. MCP-серверы — тот самый случай.”

Если захотите добавить свой сервер или пофиксить баг — код на GitHub (ссылка в моем профиле). И не забудьте прочитать про LM Studio MCP — там альтернативный подход без облачных моделей.

Подписаться на канал