Skip to main content
Glama
Weone404

weone-daily-post

by Weone404

weone-daily-post — удалённый MCP-сервер

Бэкенд публикаций для ежедневного поста We One Aviation в Instagram + Facebook. Это не сохраняющий состояние MCP-сервер на базе Streamable HTTP: Claude выполняет интеллектуальную часть (выбор темы, формулировки, подпись), а этот сервис — побочные эффекты (история, рендеринг изображений, хранилище, Graph API). Изображения вёрстаются, а не генерируются: текст на постере — это ровно тот текст, который был передан.

Claude ──POST /mcp (Bearer)──▶ Render web service (Node 20, Express)
                                 ├─ Supabase  posts table + post-images bucket
                                 ├─ Chromium      HTML template → JPEG
                                 └─ Meta Graph  IG container/publish, FB photos

Инструменты

Инструмент

Назначение

get_past_topics()

История от новых к старым, максимум 200 записей: {id, topic, category, status, created_at}. Прочитайте её перед выбором темы.

reserve_topic(topic, category)

Вставляет status='reserved', возвращает {id}. categorynews, subject, career. Повторная тема завершается ошибкой duplicate_topic.

render_post({template, headline, points, footer?, eyebrow?, slug?})

Преобразует брендированный HTML-шаблон в JPEG формата sRGB точного размера, загружает его и возвращает блок изображения плюс публичный URL.

publish_socials(image_url, caption, hashtags, history_id)

Публикует в Instagram, затем в Facebook, фиксирует результат.

mark_draft(history_id, image_url, caption)

Теневой режим: сохраняет готовый пост как draft без публикации.

check_token()

Дней до истечения токена Meta + выданные разрешения.

Каждый инструмент возвращает JSON. Успех — {"ok": true, ...}; ошибка — результат MCP-ошибки, содержащий {"ok": false, "error": {code, message, retryable, details}}. Ничто не выбрасывает в вызывающую сторону сырой стек-трейс.

render_post

template — одно из news, subject или career — те же три категории, которые использует таблица posts.

Поле

Лимит

Примечания

headline

60 символов

Barlow 700, до 3 строк. Регистр как в предложении, а не как в заголовке.

points

3–4 пункта, по 90 символов

Barlow 400, по одному золотому маркеру у каждого

eyebrow

32 символа, необязательно

Золотой, приводится к верхнему регистру через CSS, напр. NAVIGATION

footer

90 символов, необязательно

Левая часть нижней плашки, напр. DGCA · 14 Aug 2026

Он возвращает два контент-блока: блок изображения (base64 JPEG) и текстовый блок с публичным URL, именем файла, размерами, размером в байтах и временем рендеринга.

Перед загрузкой срабатывают две проверки, и обе называют проблемное поле:

  1. Лимиты длины — проверяются до того, как будет задействован Chromium; это дешёвый отказ. points[2] is 97 characters, limit is 90. Shorten it and retry.

  2. Измерение на странице — после компоновки: каждый текстовый блок — это область отсечения фиксированного размера, и если её содержимое выше или шире области, рендер отклоняется с указанием имени поля и переполнения в пикселях. Это ловит то, чего не видит подсчёт символов, например один неразрывный токен из 80 символов, который допустим по длине, но выходит за край.

Когда срабатывает любая из проверок, ничего не загружается, поэтому отказ стоит секунды, и исправление всегда одно — «сократить указанное поле».

Блок изображения по-прежнему возвращается, чтобы вы могли прочитать формулировку в контексте, но он больше не является проверкой корректности: фиксированный шаблон не может написать слово с ошибкой или выдумать схему. Наихудший допустимый ввод (заголовок из 58 символов плюс четыре пункта по 90 символов, самые широкие допустимые eyebrow и footer) проверен — он помещается во все три шаблона.

publish_socials, по шагам

  1. Выполните HEAD для image_url и проверьте 200 + content-type: image/jpeg. Meta загружает этот URL на серверной стороне, и плохой URL там приводит к непрозрачному сбою часами позже. (Если хранилище отклоняет HEAD, вместо этого выполняется ranged GET на один байт.)

  2. Instagramfull_caption = caption + "\n\n" + hashtags.join(' '), ограничена 2200 символами. Отбрасываются только хештеги, с конца; тело подписи никогда не усекается. Если одно тело превышает 2200, вызов завершается ошибкой caption_too_long до публикации чего-либо. POST {IG_USER_ID}/media → опрос GET {container}?fields=status_code,status раз в секунду до 60 секунд → публикация только при FINISHED. При ERROR строка status возвращается дословно, потому что это единственное место, где Meta объясняет, что ей не понравилось.

  3. FacebookPOST {FB_PAGE_ID}/photos с url и message. Выполняется независимо от результата Instagram.

  4. Запись — в строке posts появляются ig_post_id, fb_post_id, image_url и status = published (оба), partial (один) или failed (ни один).

Возвращает {ig_post_id, fb_post_id, status, errors: [...]}. Сбой одной платформы никогда не проглатывается: он появляется в errors[] с указанием платформы, этапа, на котором произошёл сбой, и собственных полей Meta code / error_subcode / message.

Related MCP server: Social Analytics MCP Server

Переменные окружения

Переменная

Обязательная

Что это

MCP_AUTH_TOKEN

да

Общий секрет для /mcp. Коннектор должен отправлять Authorization: Bearer <value>. Если переменная не задана, сервер всё равно запускается и отдаёт /health, но отклоняет каждый запрос к /mcp с кодом 500 — он закрыт отказом, а не открыт. Сгенерируйте значение командой node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".

SUPABASE_URL

да

https://<project-ref>.supabase.co.

SUPABASE_SERVICE_KEY

да

Служебный ключ. Обходит RLS — только на стороне сервера. Никогда не помещайте его в конфигурацию коннектора.

META_GRAPH_VERSION

нет (по умолчанию v23.0)

Версия Graph API, используемая для каждого вызова.

IG_USER_ID

для публикации

ID аккаунта Instagram Business (число, не @handle).

FB_PAGE_ID

для публикации

ID страницы Facebook, связанной с этим аккаунтом Instagram.

META_PAGE_ACCESS_TOKEN

для публикации

Долгоживущий Page access token с правами instagram_basic, instagram_content_publish, pages_show_list, pages_read_engagement, pages_manage_posts. Истекает примерно через 60 дней — check_token() показывает, сколько осталось.

PORT

нет

Render задаёт это. По умолчанию 10000.

MAX_INLINE_IMAGE_BYTES

нет (по умолчанию 1400000)

Размер, при превышении которого встроенное base64-превью уменьшается.

CHROMIUM_EXECUTABLE_PATH

нет

Явный путь к бинарнику Chrome/Chromium. Переопределяет значение по умолчанию для платформы.

CHROMIUM_SINGLE_PROCESS

нет

Установите 1, чтобы принудительно включить --single-process. Жертвует переиспользованием браузера — один рендер на запуск. См. Жизненный цикл браузера.

Скопируйте .env.example в .env для локальных запусков. .env игнорируется git — пусть так и остаётся.

Настройка

1. Supabase

Выполните migrations/001_init.sql в SQL-редакторе (или supabase db push). Он идемпотентен и создаёт:

  • таблицу posts с CHECK-ограничениями и уникальным индексом на topic — этот индекс защищает от повторов, поэтому повторный резерв должен завершаться ошибкой,

  • индексы по created_at desc и status,

  • RLS на posts без политик (доступ только через служебный ключ),

  • публичный storage-бакет post-images и его политику публичного чтения. Публичное чтение обязательно: Meta сама загружает JPEG и не может предъявить учётные данные.

2. Meta

Вам нужен аккаунт Instagram Business или Creator, связанный со страницей Facebook, и долгоживущий токен страницы с перечисленными выше правами. Подтвердите его с помощью check_token() перед первым запуском — истёкший токен является самой частой причиной неудачного утра.

3. Деплой на Render

С render.yaml (Blueprint):

  1. Запушьте этот репозиторий в GitHub.

  2. Дашборд Render → NewBlueprint → выберите репозиторий. Он читает render.yaml: Node 20, npm ci && npm run build, npm start, проверка здоровья на /health.

  3. Render запросит значения для каждой переменной, у которой sync: false. Вставьте их.

  4. Задеплойте, затем проверьте логи на наличие server.listening ... auth=configured. auth=MISSING означает, что MCP_AUTH_TOKEN не был задан.

Вручную:

  1. NewWeb Service → подключите репозиторий.

  2. Runtime Node, сборка npm ci && npm run build, запуск npm start.

  3. Путь проверки здоровья /health.

  4. Добавьте переменные окружения из таблицы выше, плюс NODE_VERSION=20.

Проверка:

curl https://<your-service>.onrender.com/health
# {"status":"ok","server":{...},"tools":[...six...],"uptime_s":3}

Используйте план Starter, а не бесплатный. Chromium требует примерно 400 МБ резидентной памяти поверх Node, а бесплатный инстанс — 512 МБ — он получит OOM в середине рендеринга, и сбой проявится как мёртвый воркер, а не полезная строка лога. Бесплатный также засыпает после бездействия, так что первый вызов инструмента за день оплачивается холодным стартом в 30–60 секунд сверху. render.yaml устанавливает starter по обеим причинам.

Загрузка браузера не нужна на этапе сборки: @sparticuz/chromium поставляет свой собственный бинарник как зависимость, так что npm ci && npm run build — это вся сборка. Этот шаг сборки также копирует src/templates/ в dist/ — tsc генерирует только .ts, так что без этого сервер запускается нормально, а затем падает при первом рендере с отсутствующим файлом шаблона.

4. Подключение к Claude

Эндпоинт:

https://<your-service>.onrender.com/mcp

с заголовком:

Authorization: Bearer <MCP_AUTH_TOKEN>

Claude Code / Cowork CLI:

claude mcp add --transport http weone-social \
  https://<your-service>.onrender.com/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

.mcp.json (в рамках проекта, закоммичен без токена):

{
  "mcpServers": {
    "weone-social": {
      "type": "http",
      "url": "https://<your-service>.onrender.com/mcp",
      "headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
    }
  }
}

В диалоге custom connector в Claude desktop/web вставьте тот же URL /mcp и поместите bearer-токен в поле заголовка запроса. Аутентификация только через заголовок — это осознанное решение: токен никогда не принимается как параметр запроса, потому что URL попадают в логи прокси и историю браузера.

Инструкции для ежедневных постов (правила бренда, запрещённые утверждения, ротация категорий, спецификация изображений, чек-лист QA) живут в навыке weone-daily-post, а не в этом сервере. Этот сервис намеренно не содержит редакционной политики.

Локальная разработка

npm install
cp .env.example .env      # fill it in
npm run dev               # tsx watch, http://localhost:10000
npm run typecheck
npm run build && npm start

npm run smoke

npm run smoke                 # render all three templates, upload, print 3 URLs
npm run smoke -- --no-upload  # render locally only, no credentials needed

Рендерит по одному экземпляру каждого шаблона, записывает все три JPEG в ./out, загружает их, проверяет каждый публичный URL через HEAD и выводит три ссылки. Затем доказывает, что обе защиты всё ещё срабатывают. Он не касается ни одного эндпоинта Meta, так что безопасен для продакшн-учёток. Для загрузки нужны только SUPABASE_URL и SUPABASE_SERVICE_KEY; --no-upload не требует ничего.

Локальные файлы записываются до загрузки, так что при сбое Supabase всё равно остаётся что посмотреть.

Конвейер рендеринга

Безголовый Chromium загружает src/templates/{template}.html через file://, значения записываются в DOM, и страница снимается скриншотом. Один и тот же вход всегда даёт одни и те же пиксели.

  • Шаблоны находятся в src/templates/. tokens.css содержит все цвета; base.css — скелет, общий для всех трёх. Файл шаблона отличается от своих собратьев только обработкой надзаголовка и маркером пунктов (новости: золотое правило, тема: нумерованные золотые круги, карьера: золотые шевроны).

  • Шрифты размещены локально в src/templates/fonts/ (Barlow 400/600/700 для всего, Cinzel 600 только для вордмарка, латинские подмножества, OFL). Ничего не загружается во время рендеринга — сетевой вызов сделал бы вывод недетерминированным и тихо бы падал на Render, переключаясь на системный шрифт с засечками. Рендерер ждёт document.fonts.ready, а затем проверяет, что оба шрифта действительно загрузились, а не делает скриншот с запасным вариантом.

  • Никакой пользовательский текст никогда не конкатенируется в разметку. Значения вставляются через textContent и createElement, так что не нужно беспокоиться об экранировании: <script> в заголовке попадает на постер как буквальные символы.

  • Вьюпорт 1080×1350 при deviceScaleFactor: 2, так что скриншот получается 2160×2700 и уменьшается — края текста остаются чёткими.

  • sharp: resize(1080, 1350, {fit:'cover'})toColorspace('srgb')jpeg({quality: 90, chromaSubsampling: '4:4:4'}), метаданные удаляются. 4:4:4 — не украшение — 4:2:0 размазывает цветные края текста, а эти постеры — это текст.

  • Проверяет, что закодированный JPEG меньше 8 МБ и что декодированные размеры действительно соответствуют запрошенным.

  • Загружает как {yyyy-mm-dd}-{slug}-{6 hex}.jpg (дата UTC). Каждый рендер получает свой собственный ключ, и ничего никогда не перезаписываетсяupsert: false. Повторный рендер темы не может изменить картинку под постом, который уже опубликовал предыдущий URL. cacheControl равен 60 секундам по той же причине: плохой объект остаётся исправимым в течение минуты, а не закрепляется в CDN на год. Meta получает URL один раз, на стороне сервера, сразу после загрузки, так что длинный кэш не нужен. Объекты накапливаются; хранилище гораздо дешевле, чем устаревшее изображение на живом посте.

Поведение макета

Тип масштабируется под количество пунктов. Три пункта дают заголовок 68px и тело 36px; четыре — 60px и 32px. Это делается в CSS с помощью :has(), так что решение о макете полностью живёт в шаблоне, а рендерер ни знает, ни заботится об этом. Защита от переполнения срабатывает после масштабирования, так что измеряется именно масштабированный результат.

Блок контента вертикально центрируется между линией заголовка и нижней панелью. Одних фиксированных отступов недостаточно для заполнения цели, потому что количество текста варьируется, поэтому три эластичных элемента делят оставшееся пространство: полоса сверху, полоса снизу и промежуток под заголовком. Полосы жёстко ограничены 150px, что и обеспечивает «отсутствие больших пустых полей»; как только они достигают предела, излишек уходит в промежуток под заголовком, где читается как воздух, а не дыра.

Измеренное вертикальное заполнение на репрезентативном контенте: 74–79%, полосы 99–124px. Намеренно разреженный случай (однострочный заголовок, три однострочных пункта) даёт 68.8% с полосами на пределе 150px — при жёстком ограничении полос это арифметический максимум для такого малого количества текста, и поднять его дальше означало бы разнести пункты так далеко, что они перестали бы читаться как список.

Заголовки — Barlow 700 в предложном регистре, интерлиньяж 1.1, трекинг −0.5px. Cinzel остаётся только в вордмарке «WE ONE AVIATION». Предложный регистр не enforced в коде — механическое приведение заголовка к нижнему регистру испортило бы DGCA, ATPL и AAI — поэтому он указан в описании поля headline.

Каждый шаблон несёт встроенный плоский SVG-акцент: широкая диагональная линия (новости), концентрические дуги компаса (тема), восходящий стек шевронов (карьера). Золото 7%, выходит за нижний правый край за текстом. Они существуют, чтобы придать композиции вес в миниатюре, и слишком бледны, чтобы влиять на контраст текста.

Акцент находится внутри .anchor-wrap, бокса, прикреплённого к холсту с overflow: hidden. Без него абсолютно позиционированная графика свисает за нижний край, учитывается в body.scrollHeight, и защита от переполнения отклоняет каждый рендер с постоянным переполнением страницы на 160px.

Логотип

src/templates/assets/logo.png — это предоставленная связка: звезда/самолёт над вордмарком «WE ONE AVIATION». Шапка сама рендерит этот вордмарк шрифтом Cinzel, поэтому scripts/prepare-logo.mjs создаёт logo-mark.png — только марку — чтобы не печатать название бренда дважды. Он находит горизонтальные полосы непрозрачных пикселей и сохраняет самую высокую, так что повторный экспорт логотипа в другом разрешении всё равно работает. После замены logo.png:

npm run prepare-logo

Жизненный цикл браузера и память

Один Chromium используется на всё время жизни процесса и перезапускается только при обрыве связи. Запуск занимает около секунды и несколько сотен МБ — слишком много, чтобы повторять для каждого поста.

Память рендера. Chromium требует примерно 400 МБ резидентной памяти поверх Node. Бесплатный инстанс — 512 МБ и получит OOM под этим — развёртывание умирает в середине рендера без полезной строки лога. Используйте план Starter. Если вы вынуждены остаться на бесплатном, ожидайте перезапусков и считайте первый рендер после каждого из них холодным стартом.

Откуда берётся бинарник, зависит от хоста:

Хост

Источник

CHROMIUM_EXECUTABLE_PATH задан

этот путь, всегда побеждает

Linux (Render)

@sparticuz/chromium, который поставляет свой собственный бинарник, так что нет загрузки браузера на этапе сборки

macOS / dev

всё, что уже закэшировано в playwright-core (npx playwright-core install chromium)

--single-process намеренно не используется. Он несовместим с повторным использованием одного браузера: закрытие BrowserContext под этим флагом разрушает весь браузер, так что второй рендер падает с ошибкой "Target page, context or browser has been closed". Измерено на этом коде: с ним выживает 1 из 3 контекстов, без него — 3 из 3. Повторное использование — более ценная половина компромисса. Установите CHROMIUM_SINGLE_PROCESS=1, чтобы принудительно включить его, если хост когда-либо потребует, и ожидайте один рендер на запуск.

Обработка ошибок

Код

Значение

bad_input

Аргументы не прошли валидацию.

duplicate_topic

Тема уже существует. Работает как задумано — выберите другую.

not_found

Нет строки posts для этого history_id. Был ли вызван reserve_topic?

db_error / storage_error

Supabase сказал нет. details содержит код Postgres.

image_generation_failed / image_too_large

Провайдер или проблема с sharp.

image_url_unreachable

URL, который Meta будет получать, недоступен как JPEG.

caption_too_long

Только тело подписи превышает 2200 символов. Хэштеги обрезаются автоматически; тело — никогда.

meta_error

Graph API. details содержит code, error_subcode, type, fbtrace_id, без изменений.

timeout

Что-то превысило свой бюджет (изображение 60с, опрос контейнера 60с, Graph 30с).

config_error

Отсутствует обязательная переменная окружения. retryable: false.

Коды Meta 190 и 200 никогда не повторяются. 190 — истёкший или недействительный токен, 200 — отсутствующее разрешение; оба требуют человека, и повторные попытки только сжигают лимит скорости, скрывая реальную причину. Эти ошибки возвращаются с retryable: false и заметкой needs_human, объясняющей, что делать.

Каждый вызов инструмента логирует tool.start и tool.ok/tool.error с длительностью, а каждый вызов Graph логирует graph.call с методом, эндпоинтом, статусом и временем в мс — так что просмотрщик логов Render достаточен для реконструкции запуска.

Устранение неполадок

Симптом

Причина

401 при каждом запросе

Заголовок отсутствует, или токен не совпадает с MCP_AUTH_TOKEN.

500 config_error на /mcp, /health в порядке

MCP_AUTH_TOKEN не задан на сервисе.

image_url_unreachable

Бакет post-images не публичный, или загрузка молча завершилась ошибкой. Запустите npm run smoke.

Контейнер IG завис на IN_PROGRESS в течение 60 с

Meta не может получить изображение, или оно работает медленно. Сначала проверьте URL в браузере.

meta_error код 190

Токен истёк. Выпустите новый долгоживущий токен страницы. check_token() предупреждает за ≤7 дней.

meta_error код 200

Отсутствует область — повторно предоставьте instagram_content_publish / pages_manage_posts.

duplicate_topic

Это не ошибка. Тема уже есть в posts.

Первый вызов дня истекает по таймауту

Холодный старт бесплатного тарифа. Сначала обратитесь к /health или перейдите на starter.

Воркер умирает во время рендера, без ошибки

Нехватка памяти. Chromium требуется ~400 МБ; бесплатный инстанс — 512 МБ. Перейдите на starter.

bad_input с указанием поля

Работает как задумано. Сократите указанное поле и вызовите render_post снова — ничего не было загружено.

Self-hosted fonts failed to load

src/templates/fonts/ не попали в dist/. Повторно запустите npm run build; рендер отклоняется, а не отправляется с запасным шрифтом с засечками.

Target page, context or browser has been closed

Установлен CHROMIUM_SINGLE_PROCESS=1. Этот флаг разрешает только один рендер за запуск — снимите его.

Файл шаблона не найден на Render

npm run build был пропущен, поэтому dist/templates/ отсутствует.

Макет

src/
  server.ts            Express, bearer auth, /health, POST /mcp
  config.ts            Lazy env resolution, constants
  log.ts               Timed stdout logging
  errors.ts            AppError / MetaError, the no-retry rule for 190 & 200
  supabase.ts          posts CRUD + storage upload
  meta.ts              Graph client, IG container flow, FB photos, debug_token
  image.ts             Chromium lifecycle, template render, overflow guard, sharp
  tools/
    register.ts        Timing, error envelope, content-block shaping
    get_past_topics.ts reserve_topic.ts render_post.ts
    publish_socials.ts mark_draft.ts check_token.ts
    index.ts
  templates/
    tokens.css base.css          design tokens + shared skeleton
    news.html subject.html career.html
    fonts/     barlow-400/600/700, cinzel-600 (woff2, self-hosted)
    assets/    logo.png (supplied), logo-mark.png (derived)
migrations/001_init.sql
scripts/
  smoke.ts           render all three, upload, prove both guards fire
  copy-templates.mjs build step: tsc emits only .ts, templates must reach dist/
  prepare-logo.mjs   derives logo-mark.png from logo.png
render.yaml
F
license - not found
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables AI assistants to manage Instagram and Threads accounts — publish content, handle comments, view insights, search hashtags, and manage DMs through the Meta Graph API.
    59
    46
    10
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    MCP server to safely publish posts to multiple Facebook Pages via Meta Graph API, with built-in guardrails for brand voice, banned topics, image requirements, and anti-duplication.
    4
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server for automatic Instagram publishing — single image, carousel and Reels — via the official Instagram Graph API.
    40

View all related MCP servers

Related MCP Connectors

  • Create, schedule and publish social posts to TikTok, Instagram, Facebook and YouTube.

  • Schedule and publish social posts to 11 platforms with media, campaigns, analytics and AI captions

  • Boost posts and launch community growth campaigns from your AI assistant. OAuth, credit-billed.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Weone404/mcp-content-cron'

If you have feedback or need assistance with the MCP directory API, please join our Discord server