weone-daily-post
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 photosHerramientas
Herramienta | Propósito |
| Historial del más reciente al más antiguo, máximo 200 filas: |
| Inserta |
| 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. |
| Publica en Instagram y luego en Facebook, registra el resultado. |
| Modo sombra: registra la publicación terminada como |
| 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 |
| 60 caracteres | Barlow 700, hasta 3 líneas. Mayúscula inicial, no título. |
| 3–4 elementos, 90 caracteres cada uno | Barlow 400, un marcador dorado cada uno |
| 32 caracteres, opcional | Dorado, en mayúsculas por CSS, p. ej. |
| 90 caracteres, opcional | Lado izquierdo de la barra de pie, p. ej. |
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:
Límites de longitud, verificados antes de tocar Chromium — el rechazo barato.
points[2] is 97 characters, limit is 90. Shorten it and retry.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
HEADa laimage_urly verifica200+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 rechazaHEADrecibe unGETde rango de un byte en su lugar.)Instagram —
full_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 concaption_too_longantes de publicar nada.POST {IG_USER_ID}/media→ consultaGET {container}?fields=status_code,statusuna vez por segundo hasta 60s → publica solo enFINISHED. EnERROR, la cadenastatusse devuelve textualmente, porque es el único lugar donde Meta explica lo que no le gustó.Facebook —
POST {FB_PAGE_ID}/photosconurlymessage. Se intenta independientemente del resultado de Instagram.Registro — la fila de
postsrecibeig_post_id,fb_post_id,image_urlystatus=published(ambos),partial(uno) ofailed(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 |
| sí | Secreto compartido para |
| sí |
|
| sí | Clave de rol de servicio. Omite RLS — solo del lado del servidor. Nunca la pongas en la configuración del conector. |
| no (por defecto | Versión de Graph API usada para cada llamada. |
| para publicar | ID de cuenta de Instagram Business (un número, no el @handle). |
| para publicar | ID de la página de Facebook vinculada a esa cuenta de Instagram. |
| para publicar | Token de acceso de Página de larga duración con |
| no | Render lo configura. Por defecto 10000. |
| no (por defecto | Tamaño por encima del cual la vista previa base64 en línea se reduce. |
| no | Ruta explícita a un binario de Chrome/Chromium. Anula el valor predeterminado por plataforma. |
| no | Configúralo a |
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
postscon las restricciones de verificación y el índice único entopic— ese índice es la guarda de repetición, así que una reserva duplicada debe fallar,índices de
created_at descystatus,RLS habilitado en
postssin políticas (solo la clave de servicio entra),el bucket de almacenamiento público
post-imagesmá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):
Sube este repositorio a GitHub.
Panel de Render → New → Blueprint → elige el repositorio. Lee
render.yaml: Node 20,npm ci && npm run build,npm start, verificación de salud en/health.Render solicita cada variable con
sync: false. Pégalas.Despliega y luego revisa los registros para
server.listening ... auth=configured.auth=MISSINGsignifica queMCP_AUTH_TOKENno se configuró.
Manualmente:
Nuevo → Web Service → conectar el repositorio.
Runtime Node, build
npm ci && npm run build, inicionpm start.Ruta de health check
/health.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/mcpcon 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 startnpm run smoke
npm run smoke # render all three templates, upload, print 3 URLs
npm run smoke -- --no-upload # render locally only, no credentials neededRenderiza 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.csscontiene todos los colores;base.csscontiene 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 adocument.fonts.readyy 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
textContentycreateElement, 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ás —upsert: false. Re-renderizar un tema no puede cambiar la imagen bajo un post que ya publicó la URL anterior.cacheControles 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 conoverflow: hidden. Sin él, el gráfico posicionado absolutamente sobresale por el borde inferior, cuenta parabody.scrollHeight, y el guard de desbordamiento rechaza cada renderizado con un desbordamiento de página constante de 160px.
El logo
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-logoCiclo 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 |
| esa ruta, siempre gana |
Linux (Render) |
|
macOS / dev | lo que |
--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 |
| Los argumentos fallaron la validación. |
| El tema ya existe. Funciona como está diseñado — elige otro. |
| No hay fila |
| Supabase dijo que no. |
| Problema del proveedor o de sharp. |
| La URL que Meta obtendría no es un JPEG alcanzable. |
| Solo el cuerpo del caption supera los 2200 caracteres. Los hashtags se recortan automáticamente; el cuerpo nunca. |
| Graph API. |
| Algo superó su presupuesto (imagen 60s, poll del contenedor 60s, Graph 30s). |
| Falta una variable de entorno requerida. |
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 |
| Falta el header, o el token no coincide con |
|
|
| El bucket |
Contenedor de IG atascado en | Meta no puede obtener la imagen, o está tardando. Comprueba la URL en un navegador primero. |
| Token caducado. Emite un nuevo token de Página de larga duración. |
| Falta el scope — vuelve a conceder |
| No es un error. El tema ya está en |
La primera llamada del día agota el tiempo | Arranque en frío del plan gratuito. Llama a |
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 |
| Funciona como se espera. Acorta el campo nombrado y llama a |
|
|
|
|
Archivo de plantilla no encontrado en Render | Se omitió |
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.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