Skip to main content
Glama
ESkuratov

MCP Content Publisher

by ESkuratov
README.md
# MCP Content Publisher

MCP-сервер для публикации контента и сбора метрик на социальных платформах.
Реализует протокол [MCP (Model Context Protocol)](https://modelcontextprotocol.io/).

## Платформы

| Платформа | Публикация | Метрики | Статус |
|---|---|---|---|
| **Telegram** | ✅ Bot API (sendMessage / sendPhoto) | 🚧 Заглушка (V2: Telethon) | Работает, протестировано |
| **YouTube** | 🚧 Заглушка | 🚧 Заглушка | План |
| **Instagram** | 🚧 Заглушка | 🚧 Заглушка | План |

## Установка

```bash
# Установка через uv
uv sync

# С тестовыми зависимостями
uv sync --group test
```

## Настройка

Скопируйте `.env.example` в `.env` и укажите токен бота:

```bash
cp .env.example .env
```

```
# TELEGRAM
TELEGRAM_BOT_TOKEN="токен_от_BotFather"
```

Токен создаётся через [@BotFather](https://t.me/BotFather) в Telegram.

## Запуск

```bash
# stdio (для интеграции с MCP-хостами — Claude Desktop, Cline и др.)
uv run mcp-content-publisher

# SSE (для отладки и удалённого доступа)
uv run mcp-content-publisher --transport sse --host 127.0.0.1 --port 8001
```

## Деплой на VPS (Docker)

### Локальная сборка и проверка

```bash
# Сборка образа
docker build -t mcp-content-publisher .

# Запуск контейнера (порт только на localhost)
docker run -d --name mcp-content-publisher \
  -p 127.0.0.1:8001:8001 \
  --env-file .env \
  mcp-content-publisher

# Проверка healthcheck
docker ps
```

### Через docker-compose

```bash
# На VPS: скопировать проект, создать .env
cp .env.example .env
# → вписать TELEGRAM_BOT_TOKEN

# Сборка и запуск
docker compose up -d --build

# Логи
docker compose logs -f

# Остановка
docker compose down
```

### Если VPS не достаёт до Telegram (прокси)

Симптом: `publish_post` возвращает `HTTP request failed:` (пустая ошибка), хотя
остальной интернет с сервера работает. Часто это **маршрутизация провайдера к
подсетям Telegram**, а не Docker. Быстрая проверка с хоста (таймаут → прокси нужен):

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://api.telegram.org/bot<TOKEN>/getMe
```

Решение — направить исходящий HTTPS контейнера через прокси (должен принимать
HTTP CONNECT или SOCKS5). Добавьте в `.env`:

```
HTTPS_PROXY="http://user:pass@proxy_host:port"
HTTP_PROXY="http://user:pass@proxy_host:port"
NO_PROXY="127.0.0.1,localhost"
```

> `NO_PROXY=127.0.0.1,localhost` обязателен — иначе healthcheck (SSE на
> localhost) тоже уйдёт через прокси. Публикация медиа (multipart → Bot API)
> идёт тем же путём — прокси покрывает и её.

Пересоздать контейнер, чтобы применить `.env`:

```bash
docker compose up -d --force-recreate
```

Проверка изнутри контейнера (ожидается `200`):

```bash
docker exec mcp-content-publisher python -c \
  "import urllib.request,os; r=urllib.request.urlopen('https://api.telegram.org/bot'+os.environ['TELEGRAM_BOT_TOKEN']+'/getMe', timeout=15); print(r.status)"
```

### Подключение MCP-клиента (через SSH-туннель)

> **Безопасность:** сервер не имеет аутентификации, поэтому **не должен быть доступен снаружи**. В Docker-конфиге порт проброшен только на `127.0.0.1`, а сервер слушает только localhost. Доступ — исключительно через SSH-туннель.

На локальной машине поднимите туннель до VPS:

```bash
ssh -L 8001:127.0.0.1:8001 root@<IP-VPS>
```

После этого сервер доступен локально:

```
http://127.0.0.1:8001/sse
```

Пример конфигурации для Claude Desktop / Cline:

```json
{
  "mcpServers": {
    "content-publisher": {
      "transport": "sse",
      "url": "http://127.0.0.1:8001/sse"
    }
  }
}
```

### Внешний доступ (не рекомендуется)

Если всё же нужно открыть сервер наружу (например, за reverse-proxy с basic-auth), потребуется:

1. Пробросить порт на все интерфейсы в `docker-compose.yml`:
   ```yaml
   ports:
     - "8001:8001"
   ```
2. Разрешить внешний Host-заголовок в `.env` (MCP SDK блокирует незнакомые Host заголовки кодом `421` — защита от DNS-rebinding):
   ```
   MCP_ALLOWED_HOSTS="<IP-или-домен>:*"
   ```
   - Несколько хостов — через запятую: `"5.129.207.137:*,mcp.example.com:*"`
   - `MCP_ALLOWED_HOSTS="*"` — отключить защиту (любой Host)
   - Не задано — только localhost (по умолчанию)

`127.0.0.1`, `localhost`, `[::1]` разрешены всегда (нужны для healthcheck).

## Инструменты MCP

### `publish_post`

Опубликовать пост на указанной платформе.

**Параметры:**
- `platform` (str): `telegram` | `youtube` | `instagram`
- `text` (str): Текст поста. Поддерживает HTML-разметку (`<b>`, `<i>`, `<code>`, `<a href="...">`)
- `channel` (str): Канал для публикации — @username, chat ID или invite link
- `media_urls` (list[str], optional): изображение/видео — **URL** (`https://…`), **локальный путь** (`/app/output/cover.png`) или `file://` URL
- `schedule_at` (str, optional): Время публикации в ISO-8601

**Пример:**
```json
{
  "platform": "telegram",
  "text": "<b>Привет!</b> Это тестовый пост из MCP сервера",
  "channel": "-1001234567890"
}
```

**Ответ:**
```json
{
  "post_id": "42",
  "status": "published",
  "url": "https://t.me/channel/42",
  "error": null
}
```

**Медиа (фото):** если передать в `media_urls` локальный путь или `file://` URL —
файл загружается в Telegram через **multipart** (`sendPhoto` с `files`), внешние
URL и публичный хостинг не нужны. Это важно на серверах, где Telegram заблокирован
(см. секцию про прокси) и для локально-генерируемых обложек. HTTP(S)-URL и
`file_id` по-прежнему передаются как есть.

### `get_metrics`

Получить метрики опубликованного поста.

**Параметры:**
- `platform` (str): Платформа
- `post_id` (str): ID поста на платформе

**Ответ:**
```json
{
  "post_id": "42",
  "platform": "telegram",
  "views": 150,
  "reactions": 12,
  "reposts": 3,
  "comments": 2
}
```

> **Примечание:** Telegram Bot API не отдаёт реальные просмотры/реакции. В MVP возвращаются mock-данные. В V2 планируется Telethon (MTProto) для реальных метрик.

### `get_channel_stats`

Получить общую статистику канала.

**Параметры:**
- `platform` (str): Платформа
- `channel` (str): Имя/ID канала

**Ответ:**
```json
{
  "platform": "telegram",
  "channel": "@channel",
  "subscribers": 1000,
  "posts_this_week": 5,
  "avg_views": 200
}
```

## Форматирование текста

Telegram провайдер автоматически определяет режим разметки:

- **HTML** — если в тексте есть HTML-теги (`<b>`, `<i>`, `<code>`, `<a>`)
- **HTML (по умолчанию)** — для обычного текста (не требует экранирования)

Поддерживаемые HTML-теги в Telegram: `<b>`, `<i>`, `<u>`, `<s>`, `<code>`, `<pre>`, `<a href="...">`

## Тестирование

```bash
# Запуск всех тестов
uv run pytest tests/ -v

# Только unit-тесты
uv run pytest tests/test_publish_server.py -v

# Только интеграционные тесты (MCP протокол)
uv run pytest tests/test_mcp_server_integration.py -v
```

### Что тестируется

- **Модели** — Pydantic-схемы (PublishContent, PublishResult, PostMetrics, ChannelStats)
- **Провайдеры** — Telegram (токен, публикация, метрики), YouTube/Instagram (mock)
- **Реестр провайдеров** — синглтон, неизвестные платформы
- **MCP сервер** — регистрация инструментов, вызов через MCP протокол

## Структура проекта

```
mcp-content-publisher/
├── src/mcp_content_publisher/
│   ├── server.py          # MCP сервер (точка входа)
│   ├── models.py          # Pydantic-схемы
│   └── providers/
│       ├── base.py        # Базовый класс PublishProvider
│       ├── telegram.py    # Telegram Bot API
│       ├── youtube.py     # YouTube (заглушка)
│       └── instagram.py   # Instagram (заглушка)
├── tests/
│   ├── test_server.py              # Тесты MCP сервера
│   ├── test_publish_server.py      # Тесты провайдеров и моделей
│   └── test_mcp_server_integration.py  # Интеграционные тесты
├── .env.example
└── pyproject.toml
```

## Разработка

```bash
# Установка с dev-зависимостями
uv sync --group test

# Запуск тестов
uv run pytest

# Проверка типов
uv run mypy src/
```

TDQS

A3.6/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: publishing a post, getting metrics for a post, and getting channel statistics. No overlap or ambiguity.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern (publish_post, get_metrics, get_channel_stats), making it easy to predict functionality.

Tool Count4/5

With only 3 tools, the server is minimal but functionally complete for basic publishing and stats retrieval. Slightly limited but appropriate for its stated purpose.

Completeness2/5

The server provides publish and read operations but lacks update/delete tools for posts, and no tool to list or manage scheduled posts. Significant lifecycle gaps exist.

Maintenance

ActivitySlowing
ResponsivenessNo issues