weone-daily-post
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Инструменты
Инструмент | Назначение |
| История от новых к старым, максимум 200 записей: |
| Вставляет |
| Преобразует брендированный HTML-шаблон в JPEG формата sRGB точного размера, загружает его и возвращает блок изображения плюс публичный URL. |
| Публикует в Instagram, затем в Facebook, фиксирует результат. |
| Теневой режим: сохраняет готовый пост как |
| Дней до истечения токена Meta + выданные разрешения. |
Каждый инструмент возвращает JSON. Успех — {"ok": true, ...}; ошибка — результат MCP-ошибки, содержащий {"ok": false, "error": {code, message, retryable, details}}. Ничто не выбрасывает в вызывающую сторону сырой стек-трейс.
render_post
template — одно из news, subject или career — те же три категории, которые использует таблица posts.
Поле | Лимит | Примечания |
| 60 символов | Barlow 700, до 3 строк. Регистр как в предложении, а не как в заголовке. |
| 3–4 пункта, по 90 символов | Barlow 400, по одному золотому маркеру у каждого |
| 32 символа, необязательно | Золотой, приводится к верхнему регистру через CSS, напр. |
| 90 символов, необязательно | Левая часть нижней плашки, напр. |
Он возвращает два контент-блока: блок изображения (base64 JPEG) и текстовый блок с публичным URL, именем файла, размерами, размером в байтах и временем рендеринга.
Перед загрузкой срабатывают две проверки, и обе называют проблемное поле:
Лимиты длины — проверяются до того, как будет задействован Chromium; это дешёвый отказ.
points[2] is 97 characters, limit is 90. Shorten it and retry.Измерение на странице — после компоновки: каждый текстовый блок — это область отсечения фиксированного размера, и если её содержимое выше или шире области, рендер отклоняется с указанием имени поля и переполнения в пикселях. Это ловит то, чего не видит подсчёт символов, например один неразрывный токен из 80 символов, который допустим по длине, но выходит за край.
Когда срабатывает любая из проверок, ничего не загружается, поэтому отказ стоит секунды, и исправление всегда одно — «сократить указанное поле».
Блок изображения по-прежнему возвращается, чтобы вы могли прочитать формулировку в контексте, но он больше не является проверкой корректности: фиксированный шаблон не может написать слово с ошибкой или выдумать схему. Наихудший допустимый ввод (заголовок из 58 символов плюс четыре пункта по 90 символов, самые широкие допустимые eyebrow и footer) проверен — он помещается во все три шаблона.
publish_socials, по шагам
Выполните
HEADдляimage_urlи проверьте200+content-type: image/jpeg. Meta загружает этот URL на серверной стороне, и плохой URL там приводит к непрозрачному сбою часами позже. (Если хранилище отклоняетHEAD, вместо этого выполняется rangedGETна один байт.)Instagram —
full_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 объясняет, что ей не понравилось.Facebook —
POST {FB_PAGE_ID}/photosсurlиmessage. Выполняется независимо от результата Instagram.Запись — в строке
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
Переменные окружения
Переменная | Обязательная | Что это |
| да | Общий секрет для |
| да |
|
| да | Служебный ключ. Обходит RLS — только на стороне сервера. Никогда не помещайте его в конфигурацию коннектора. |
| нет (по умолчанию | Версия Graph API, используемая для каждого вызова. |
| для публикации | ID аккаунта Instagram Business (число, не @handle). |
| для публикации | ID страницы Facebook, связанной с этим аккаунтом Instagram. |
| для публикации | Долгоживущий Page access token с правами |
| нет | Render задаёт это. По умолчанию 10000. |
| нет (по умолчанию | Размер, при превышении которого встроенное base64-превью уменьшается. |
| нет | Явный путь к бинарнику Chrome/Chromium. Переопределяет значение по умолчанию для платформы. |
| нет | Установите |
Скопируйте .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):
Запушьте этот репозиторий в GitHub.
Дашборд Render → New → Blueprint → выберите репозиторий. Он читает
render.yaml: Node 20,npm ci && npm run build,npm start, проверка здоровья на/health.Render запросит значения для каждой переменной, у которой
sync: false. Вставьте их.Задеплойте, затем проверьте логи на наличие
server.listening ... auth=configured.auth=MISSINGозначает, чтоMCP_AUTH_TOKENне был задан.
Вручную:
New → Web Service → подключите репозиторий.
Runtime Node, сборка
npm ci && npm run build, запускnpm start.Путь проверки здоровья
/health.Добавьте переменные окружения из таблицы выше, плюс
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 startnpm 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. Если вы вынуждены остаться на бесплатном, ожидайте перезапусков и считайте первый рендер после каждого из них холодным стартом.
Откуда берётся бинарник, зависит от хоста:
Хост | Источник |
| этот путь, всегда побеждает |
Linux (Render) |
|
macOS / dev | всё, что уже закэшировано в |
--single-process намеренно не используется. Он несовместим с повторным использованием одного браузера: закрытие BrowserContext под этим флагом разрушает весь браузер, так что второй рендер падает с ошибкой "Target page, context or browser has been closed". Измерено на этом коде: с ним выживает 1 из 3 контекстов, без него — 3 из 3. Повторное использование — более ценная половина компромисса. Установите CHROMIUM_SINGLE_PROCESS=1, чтобы принудительно включить его, если хост когда-либо потребует, и ожидайте один рендер на запуск.
Обработка ошибок
Код | Значение |
| Аргументы не прошли валидацию. |
| Тема уже существует. Работает как задумано — выберите другую. |
| Нет строки |
| Supabase сказал нет. |
| Провайдер или проблема с sharp. |
| URL, который Meta будет получать, недоступен как JPEG. |
| Только тело подписи превышает 2200 символов. Хэштеги обрезаются автоматически; тело — никогда. |
| Graph API. |
| Что-то превысило свой бюджет (изображение 60с, опрос контейнера 60с, Graph 30с). |
| Отсутствует обязательная переменная окружения. |
Коды Meta 190 и 200 никогда не повторяются. 190 — истёкший или недействительный токен, 200 — отсутствующее разрешение; оба требуют человека, и повторные попытки только сжигают лимит скорости, скрывая реальную причину. Эти ошибки возвращаются с retryable: false и заметкой needs_human, объясняющей, что делать.
Каждый вызов инструмента логирует tool.start и tool.ok/tool.error с длительностью, а каждый вызов Graph логирует graph.call с методом, эндпоинтом, статусом и временем в мс — так что просмотрщик логов Render достаточен для реконструкции запуска.
Устранение неполадок
Симптом | Причина |
| Заголовок отсутствует, или токен не совпадает с |
|
|
| Бакет |
Контейнер IG завис на | Meta не может получить изображение, или оно работает медленно. Сначала проверьте URL в браузере. |
| Токен истёк. Выпустите новый долгоживущий токен страницы. |
| Отсутствует область — повторно предоставьте |
| Это не ошибка. Тема уже есть в |
Первый вызов дня истекает по таймауту | Холодный старт бесплатного тарифа. Сначала обратитесь к |
Воркер умирает во время рендера, без ошибки | Нехватка памяти. Chromium требуется ~400 МБ; бесплатный инстанс — 512 МБ. Перейдите на |
| Работает как задумано. Сократите указанное поле и вызовите |
|
|
| Установлен |
Файл шаблона не найден на Render |
|
Макет
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.yamlThis server cannot be installed
Maintenance
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
- AlicenseAqualityCmaintenanceEnables AI assistants to manage Instagram and Threads accounts — publish content, handle comments, view insights, search hashtags, and manage DMs through the Meta Graph API.594610MIT
- AlicenseAqualityDmaintenanceEnables Instagram and Facebook analytics via the Meta Graph API, offering account insights, media analytics, and post engagement data.196MIT
- AlicenseAqualityDmaintenanceMCP 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.4MIT
- FlicenseNot gradedqualityCmaintenanceMCP server for automatic Instagram publishing — single image, carousel and Reels — via the official Instagram Graph API.40
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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