Skip to main content
Glama
Weone404

weone-daily-post

by Weone404

weone-daily-post — servidor MCP remoto

Backend de publicación para la publicación diaria de Instagram + Facebook de We One Aviation. Es un servidor MCP Streamable HTTP sin estado: Claude hace el pensamiento (elección de tema, redacción, pie de foto), este servicio hace los efectos secundarios (historial, renderizado de imágenes, almacenamiento, Graph API). Las imágenes se componen tipográficamente, no se generan: el texto del póster es exactamente el texto proporcionado.

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

Herramientas

Herramienta

Propósito

get_past_topics()

Historial del más reciente al más antiguo, máximo 200 filas: {id, topic, category, status, created_at}. Léelo antes de elegir un tema.

reserve_topic(topic, category)

Inserta status='reserved', devuelve {id}. categorynews, subject, career. Un tema repetido falla con duplicate_topic.

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

Renderiza una plantilla HTML con marca a un JPEG sRGB de tamaño exacto, lo sube y devuelve un bloque de imagen más la URL pública.

publish_socials(image_url, caption, hashtags, history_id)

Publica en Instagram y luego en Facebook, registra el resultado.

mark_draft(history_id, image_url, caption)

Modo sombra: registra la publicación terminada como draft sin publicar.

check_token()

Días hasta que expire el token de Meta + alcances concedidos.

Cada herramienta devuelve JSON. El éxito es {"ok": true, ...}; el fallo es un resultado de error MCP que contiene {"ok": false, "error": {code, message, retryable, details}}. Nada lanza un stack trace crudo al llamador.

render_post

template es uno de news, subject o career — las mismas tres categorías que usa la tabla posts.

Campo

Límite

Notas

headline

60 caracteres

Barlow 700, hasta 3 líneas. Mayúscula inicial, no título.

points

3–4 elementos, 90 caracteres cada uno

Barlow 400, un marcador dorado cada uno

eyebrow

32 caracteres, opcional

Dorado, en mayúsculas por CSS, p. ej. NAVIGATION

footer

90 caracteres, opcional

Lado izquierdo de la barra de pie, p. ej. DGCA · 14 Aug 2026

Devuelve dos bloques de contenido: un bloque de imagen (JPEG en base64) y un bloque de texto con la URL pública, nombre de archivo, dimensiones, tamaño en bytes y tiempo de renderizado.

Dos guardas se ejecutan antes de que se suba nada, y ambas nombran el campo infractor:

  1. Límites de longitud, verificados antes de tocar Chromium — el rechazo barato. points[2] is 97 characters, limit is 90. Shorten it and retry.

  2. Medición en página, después del diseño — cada cuadro de texto es un cuadro de recorte de tamaño fijo, y si su contenido es más alto o más ancho que el cuadro, el renderizado se rechaza con el nombre del campo y el desbordamiento en píxeles. Esto detecta lo que un contador de caracteres no puede ver, como un token ininterrumpible de 80 caracteres que es legal en longitud pero se sale del borde.

Nada se sube cuando se activa cualquiera de las guardas, así que un rechazo cuesta un segundo y la solución es siempre "acortar el campo nombrado".

El bloque de imagen sigue llegando para que puedas leer el texto en contexto, pero ya no es una verificación de corrección: una plantilla fija no puede escribir mal una palabra o inventar un diagrama. La entrada legal en el peor caso (titular de 58 caracteres más cuatro puntos de 90 caracteres, eyebrow y footer legales más anchos) se ha verificado que cabe en las tres plantillas.

publish_socials, paso a paso

  1. HEAD a la image_url y verifica 200 + content-type: image/jpeg. Meta obtiene esta URL del lado del servidor, y una URL mala allí falla de forma opaca horas después. (Un almacén que rechaza HEAD recibe un GET de rango de un byte en su lugar.)

  2. Instagramfull_caption = caption + "\n\n" + hashtags.join(' '), limitado a 2200 caracteres. Solo se eliminan los hashtags, desde el final; el cuerpo del pie de foto nunca se trunca. Si solo el cuerpo supera los 2200, la llamada falla con caption_too_long antes de publicar nada. POST {IG_USER_ID}/media → consulta GET {container}?fields=status_code,status una vez por segundo hasta 60s → publica solo en FINISHED. En ERROR, la cadena status se devuelve textualmente, porque es el único lugar donde Meta explica lo que no le gustó.

  3. FacebookPOST {FB_PAGE_ID}/photos con url y message. Se intenta independientemente del resultado de Instagram.

  4. Registro — la fila de posts recibe ig_post_id, fb_post_id, image_url y status = published (ambos), partial (uno) o failed (ninguno).

Devuelve {ig_post_id, fb_post_id, status, errors: [...]}. Un fallo de una sola plataforma nunca se traga: aparece en errors[] con la plataforma, la etapa en la que falló y el propio code / error_subcode / message de Meta.

Related MCP server: Social Analytics MCP Server

Variables de entorno

Variable

Requerida

Qué es

MCP_AUTH_TOKEN

Secreto compartido para /mcp. El conector debe enviar Authorization: Bearer <value>. Si no está configurado, el servidor aún arranca y sirve /health, pero rechaza cada solicitud /mcp con un 500 — falla cerrado, nunca abierto. Genera uno con node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".

SUPABASE_URL

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

SUPABASE_SERVICE_KEY

Clave de rol de servicio. Omite RLS — solo del lado del servidor. Nunca la pongas en la configuración del conector.

META_GRAPH_VERSION

no (por defecto v23.0)

Versión de Graph API usada para cada llamada.

IG_USER_ID

para publicar

ID de cuenta de Instagram Business (un número, no el @handle).

FB_PAGE_ID

para publicar

ID de la página de Facebook vinculada a esa cuenta de Instagram.

META_PAGE_ACCESS_TOKEN

para publicar

Token de acceso de Página de larga duración con instagram_basic, instagram_content_publish, pages_show_list, pages_read_engagement, pages_manage_posts. Expira alrededor de 60 días — check_token() te dice cuánto queda.

PORT

no

Render lo configura. Por defecto 10000.

MAX_INLINE_IMAGE_BYTES

no (por defecto 1400000)

Tamaño por encima del cual la vista previa base64 en línea se reduce.

CHROMIUM_EXECUTABLE_PATH

no

Ruta explícita a un binario de Chrome/Chromium. Anula el valor predeterminado por plataforma.

CHROMIUM_SINGLE_PROCESS

no

Configúralo a 1 para forzar --single-process. Cuesta reutilización del navegador — un renderizado por lanzamiento. Consulta Ciclo de vida del navegador.

Copia .env.example a .env para ejecuciones locales. .env está en gitignore — mantenlo así.

Configuración

1. Supabase

Ejecuta migrations/001_init.sql en el editor SQL (o supabase db push). Es idempotente y crea:

  • la tabla posts con las restricciones de verificación y el índice único en topic — ese índice es la guarda de repetición, así que una reserva duplicada debe fallar,

  • índices de created_at desc y status,

  • RLS habilitado en posts sin políticas (solo la clave de servicio entra),

  • el bucket de almacenamiento público post-images más su política de lectura pública. La lectura pública es obligatoria: Meta obtiene el JPEG por sí mismo y no puede presentar credenciales.

2. Meta

Necesitas una cuenta de Instagram Business o Creator vinculada a una página de Facebook, y un token de página de larga duración con los alcances listados arriba. Confírmalo con check_token() antes de la primera ejecución — un token caducado es la causa única más común de una mañana fallida.

3. Desplegar en Render

Con render.yaml (Blueprint):

  1. Sube este repositorio a GitHub.

  2. Panel de Render → NewBlueprint → elige el repositorio. Lee render.yaml: Node 20, npm ci && npm run build, npm start, verificación de salud en /health.

  3. Render solicita cada variable con sync: false. Pégalas.

  4. Despliega y luego revisa los registros para server.listening ... auth=configured. auth=MISSING significa que MCP_AUTH_TOKEN no se configuró.

Manualmente:

  1. NuevoWeb Service → conectar el repositorio.

  2. Runtime Node, build npm ci && npm run build, inicio npm start.

  3. Ruta de health check /health.

  4. Añadir las variables de entorno de la tabla anterior, más NODE_VERSION=20.

Verificar:

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

Usar el plan Starter, no el gratuito. Chromium necesita aproximadamente 400 MB residentes además de Node, y la instancia gratuita es de 512 MB — se quedará sin memoria a mitad del renderizado, y el fallo aparece como un worker muerto en lugar de una línea de log útil. El plan gratuito también se duerme tras inactividad, así que la primera llamada de herramienta del día paga un arranque en frío de 30–60 segundos adicionales. render.yaml define starter por ambas razones.

No se necesita descargar el navegador en tiempo de build: @sparticuz/chromium incluye su propio binario como dependencia, así que npm ci && npm run build es todo el build. Ese paso de build también copia src/templates/ a dist/ — tsc solo emite .ts, así que sin ello el servidor arranca bien y luego falla en el primer renderizado con un archivo de plantilla ausente.

4. Conectarlo a Claude

El endpoint es:

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

con la cabecera:

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 (a nivel de proyecto, verificado sin el token):

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

En el diálogo de conector personalizado de Claude desktop/web, pegar la misma URL /mcp y poner el bearer token en el campo de cabeceras de petición. La autenticación es solo por cabecera por diseño — el token nunca se acepta como parámetro de consulta, porque las URLs acaban en logs de proxies y en el historial del navegador.

Las instrucciones de publicación diaria (reglas de marca, afirmaciones prohibidas, rotación de categorías, especificación de imágenes, checklist de QA) viven en la skill weone-daily-post, no en este servidor. Este servicio no contiene deliberadamente ninguna política editorial.

Desarrollo local

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

Renderiza una de cada plantilla, escribe los tres JPEG en ./out, los sube, hace HEAD-check de cada URL pública e imprime los tres enlaces. Luego demuestra que ambos guards siguen disparándose. No toca ningún endpoint de Meta, así que es seguro frente a credenciales de producción. La subida solo necesita SUPABASE_URL y SUPABASE_SERVICE_KEY; --no-upload no necesita nada.

Los archivos locales se escriben antes de la subida, así que un fallo de Supabase deja algo que mirar.

Pipeline de renderizado

Un Chromium headless carga src/templates/{template}.html vía file://, los valores se escriben en el DOM, y se captura la página. La misma entrada siempre produce los mismos píxeles.

  • Las plantillas viven en src/templates/. tokens.css contiene todos los colores; base.css contiene el esqueleto que comparten las tres. Un archivo de plantilla difiere de sus hermanas solo en el tratamiento del eyebrow y el marcador de puntos (news: regla dorada, subject: círculos dorados numerados, career: chevrones dorados).

  • Las fuentes están auto-alojadas en src/templates/fonts/ (Barlow 400/600/700 para todo, Cinzel 600 solo para el wordmark, subconjuntos latinos, OFL). No se obtiene nada en tiempo de renderizado — una llamada de red haría la salida no determinista y fallaría silenciosamente en Render, cayendo a una serif del sistema. El renderizador espera a document.fonts.ready y luego verifica que ambas familias realmente cargaron en lugar de capturar un fallback.

  • Ningún texto de usuario se concatena jamás en el markup. Los valores entran a través de textContent y createElement, así que no hay escapado que pueda fallar: un <script> en un titular aterriza en el póster como los caracteres literales.

  • Viewport 1080×1350 con deviceScaleFactor: 2, así que la captura es 2160×2700 y se reduce — los bordes del texto se mantienen limpios.

  • sharp: resize(1080, 1350, {fit:'cover'})toColorspace('srgb')jpeg({quality: 90, chromaSubsampling: '4:4:4'}), metadatos eliminados. 4:4:4 no es decoración — 4:2:0 emborrona los bordes del texto en color, y estos pósters son texto.

  • Verifica que el JPEG codificado está por debajo de 8 MB y que las dimensiones decodificadas son realmente las solicitadas.

  • Sube como {yyyy-mm-dd}-{slug}-{6 hex}.jpg (fecha UTC). Cada renderizado recibe su propia clave y nada se sobrescribe jamásupsert: false. Re-renderizar un tema no puede cambiar la imagen bajo un post que ya publicó la URL anterior. cacheControl es de 60s por la misma razón: un objeto defectuoso sigue siendo corregible en menos de un minuto en lugar de quedar fijado en un CDN durante un año. Meta obtiene la URL una vez, del lado del servidor, momentos después de la subida, así que nada necesita la caché larga. Los objetos se acumulan; el almacenamiento es mucho más barato que una imagen obsoleta en un post en vivo.

Comportamiento del layout

El tipo escala según el número de puntos. Tres puntos reciben un titular de 68px y cuerpo de 36px; cuatro reciben 60px y 32px. Esto se hace en CSS con :has(), así que la decisión de layout vive enteramente en la plantilla y el renderizador ni lo sabe ni le importa. El guard de desbordamiento se ejecuta después del escalado, así que el resultado escalado es lo que se mide.

El bloque de contenido está centrado verticalmente entre la regla de cabecera y la barra de pie. Los gaps fijos por sí solos no pueden mantener un objetivo de relleno porque la cantidad de texto varía, así que tres elementos elásticos comparten el espacio sobrante: una banda arriba, una banda abajo, y el gap bajo el titular. Las bandas tienen un tope duro de 150px, que es lo que impone "sin margen vacío grande"; una vez que llegan al tope, el excedente va al gap del titular donde se lee como espacio de respiración en lugar de un agujero.

Relleno vertical medido en contenido representativo: 74–79%, bandas 99–124px. Un caso deliberadamente escaso (titular de una línea, tres puntos de una línea) se sitúa en 68.8% con las bandas en su tope de 150px — con un tope duro en las bandas ese es el máximo aritmético para tan poco texto, y elevarlo más significaría separar los puntos tanto que dejarían de leerse como lista.

Los titulares son Barlow 700 en sentence case, interlineado 1.1, letter-spacing −0.5px. Cinzel sobrevive solo en el wordmark "WE ONE AVIATION". El sentence case no se impone en código — poner en minúsculas mecánicamente un titular destrozaría DGCA, ATPL y AAI — así que se especifica en la descripción del campo headline en su lugar.

Cada plantilla lleva un acento SVG plano inline: una regla diagonal ancha (news), arcos de brújula concéntricos (subject), una pila de chevrones ascendente (career). Dorado al 7%, sangrando por el borde inferior derecho detrás del texto. Existen para dar peso a la composición en tamaño miniatura y son demasiado tenues para afectar al contraste del texto.

El acento está dentro de .anchor-wrap, una caja anclada al lienzo con overflow: hidden. Sin él, el gráfico posicionado absolutamente sobresale por el borde inferior, cuenta para body.scrollHeight, y el guard de desbordamiento rechaza cada renderizado con un desbordamiento de página constante de 160px.

src/templates/assets/logo.png es el lockup suministrado: la marca de estrella/aeronave sobre un wordmark "WE ONE AVIATION". La cabecera renderiza ese wordmark en Cinzel, así que scripts/prepare-logo.mjs deriva logo-mark.png — solo la marca — para evitar imprimir el nombre de la marca dos veces. Encuentra las bandas horizontales de píxeles no transparentes y conserva la más alta, así que re-exportar el logo a otra resolución sigue funcionando. Después de reemplazar logo.png:

npm run prepare-logo

Ciclo de vida del navegador y memoria

Un Chromium se comparte durante toda la vida del proceso y se relanza solo si se desconecta. Lanzarlo cuesta aproximadamente un segundo y unos cientos de MB, demasiado para repetirlo por cada post.

Memoria de renderizado. Chromium necesita aproximadamente 400 MB residentes además de Node. La instancia gratuita es de 512 MB y se quedará sin memoria bajo eso — el deploy muere a mitad del renderizado sin una línea de log útil. Usar el plan Starter. Si debes quedarte en el gratuito, espera reinicios y trata el primer renderizado después de cada uno como un arranque en frío.

De dónde viene el binario depende del host:

Host

Fuente

CHROMIUM_EXECUTABLE_PATH definido

esa ruta, siempre gana

Linux (Render)

@sparticuz/chromium, que incluye su propio binario, así que no hay descarga del navegador en tiempo de build

macOS / dev

lo que playwright-core ya tenga en caché (npx playwright-core install chromium)

--single-process se omite deliberadamente. Es incompatible con reutilizar un navegador: cerrar un BrowserContext bajo esa bandera derriba todo el navegador, así que el segundo renderizado falla con "Target page, context or browser has been closed". Medido en este código: 1 de 3 contextos sobreviven con él, 3 de 3 sin él. La reutilización es la mitad más valiosa del intercambio. Define CHROMIUM_SINGLE_PROCESS=1 para forzarlo de nuevo si un host lo exige, y espera un renderizado por lanzamiento.

Manejo de errores

Código

Significado

bad_input

Los argumentos fallaron la validación.

duplicate_topic

El tema ya existe. Funciona como está diseñado — elige otro.

not_found

No hay fila posts para ese history_id. ¿Se llamó a reserve_topic?

db_error / storage_error

Supabase dijo que no. details lleva el código de Postgres.

image_generation_failed / image_too_large

Problema del proveedor o de sharp.

image_url_unreachable

La URL que Meta obtendría no es un JPEG alcanzable.

caption_too_long

Solo el cuerpo del caption supera los 2200 caracteres. Los hashtags se recortan automáticamente; el cuerpo nunca.

meta_error

Graph API. details tiene code, error_subcode, type, fbtrace_id, sin cambios.

timeout

Algo superó su presupuesto (imagen 60s, poll del contenedor 60s, Graph 30s).

config_error

Falta una variable de entorno requerida. retryable: false.

Los códigos 190 y 200 de Meta nunca se reintentan. 190 es un token caducado o inválido, 200 es un permiso ausente; ambos necesitan a un humano, y reintentar solo quema límite de tasa mientras oculta la causa real. Esos errores vuelven con retryable: false y una nota needs_human que dice qué hacer.

Cada llamada de herramienta registra tool.start y tool.ok/tool.error con una duración, y cada llamada a Graph registra graph.call con método, endpoint, estado y ms transcurridos — así que el visor de logs de Render es suficiente para reconstruir una ejecución.

Solución de problemas

Síntoma

Causa

401 en cada solicitud

Falta el header, o el token no coincide con MCP_AUTH_TOKEN.

500 config_error en /mcp, /health bien

MCP_AUTH_TOKEN no está configurado en el servicio.

image_url_unreachable

El bucket post-images no es público, o la subida falló silenciosamente. Ejecuta npm run smoke.

Contenedor de IG atascado en IN_PROGRESS 60s

Meta no puede obtener la imagen, o está tardando. Comprueba la URL en un navegador primero.

meta_error código 190

Token caducado. Emite un nuevo token de Página de larga duración. check_token() avisa con ≤7 días.

meta_error código 200

Falta el scope — vuelve a conceder instagram_content_publish / pages_manage_posts.

duplicate_topic

No es un error. El tema ya está en posts.

La primera llamada del día agota el tiempo

Arranque en frío del plan gratuito. Llama a /health primero, o muévete a starter.

El worker muere a mitad del renderizado, sin error

Falta de memoria. Chromium necesita ~400 MB; la instancia gratuita es de 512 MB. Muévete a starter.

bad_input nombrando un campo

Funciona como se espera. Acorta el campo nombrado y llama a render_post de nuevo — no se subió nada.

Self-hosted fonts failed to load

src/templates/fonts/ no llegó a dist/. Vuelve a ejecutar npm run build; el renderizado se rechaza en lugar de enviarse con una serif de respaldo.

Target page, context or browser has been closed

CHROMIUM_SINGLE_PROCESS=1 está configurado. Ese flag solo permite un renderizado por lanzamiento — desactívalo.

Archivo de plantilla no encontrado en Render

Se omitió npm run build, por lo que falta dist/templates/.

Diseño

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