linkedin-poster
linkedin-poster
Publica en el feed de tu propia cuenta de LinkedIn desde Claude Code, un shell o un trabajo cron. Un solo Cloudflare Worker, sin dependencias, tres superficies: un servidor MCP para que un agente pueda publicar directamente, una API HTTP para scripts y un linter de voz que se niega a publicar texto con las marcas de la prosa escrita por máquinas.
Creado porque una buena publicación merece automatizarse y una mala raya no merece publicarse. Este README tampoco contiene rayas, que es de lo que se trata.
Claude Code ──MCP──┐
├──> Worker ──> LinkedIn Posts API ──> your feed
curl / CI ──HTTP───┘ │
├──> KV (one 60-day token) + daily cron
└──> Telegram group (what happened, and what broke)Lo que no es
Esto publica como un solo miembro: tú. No es un SaaS de programación, ni una herramienta multiinquilino, y no toca la cuenta de nadie más. Todo lo que necesita es autogestionado; no interviene ningún programa de socios de LinkedIn.
Related MCP server: mcp-server-linkedin
La restricción que lo condiciona todo
Los tokens de acceso de LinkedIn duran 60 días, y los tokens de actualización programáticos están limitados a los socios aprobados de Marketing Developer Platform. Una aplicación de autoservicio nunca consigue uno. No hay forma de evitarlo, y cualquier guía que prometa automatización de LinkedIn de configurar y olvidar está equivocada.
Lo que sí hay: mientras tu token actual siga siendo válido y tengas la sesión iniciada en LinkedIn, volver a llamar al endpoint de autorización omite la pantalla de consentimiento y simplemente redirige. Así que la renovación es un clic cada siete semanas, siempre que lo hagas antes de que el token expire. Si dejas pasar la ventana, vuelves a ver la pantalla de consentimiento, que son dos clics en lugar de uno.
Por eso este servicio incluye un disparador cron. A partir de 14 días antes, coloca una alarma de renovación al principio del resumen diario, cada mañana, mientras la renovación silenciosa todavía funciona. Ver Notificaciones.
Configuración
1. En LinkedIn (manual, unos diez minutos)
Necesitas una LinkedIn Page. El portal para desarrolladores te obliga a vincular la aplicación a una y a que un administrador de la página la verifique.
Crea la aplicación en linkedin.com/developers. Anota el ID de cliente y el Secreto de cliente.
En la pestaña Productos, solicita ambos (son permisos abiertos y se aprueban al instante):
Sign In with LinkedIn using OpenID Connect →
openid,profileShare on LinkedIn →
w_member_social
En la pestaña Auth → añade la URL de redirección. Debe ser HTTPS, absoluta y coincidir con
REDIRECT_URIcarácter por carácter:https://your-worker.example.com/auth/callback
2. Lado del Worker
git clone https://github.com/stepanic/linkedin-poster.git
cd linkedin-poster
npm install
# KV to hold the token
npx wrangler kv namespace create TOKENS
npx wrangler kv namespace create TOKENS --preview
# paste both ids into wrangler.jsonc
# edit wrangler.jsonc: name, routes, REDIRECT_URI, account_id
npx wrangler secret put LINKEDIN_CLIENT_ID
npx wrangler secret put LINKEDIN_CLIENT_SECRET
npx wrangler secret put API_TOKEN # openssl rand -hex 32, guards /post and /mcp
npx wrangler secret put SETUP_KEY # openssl rand -hex 32, guards /auth/start
npx wrangler secret put NOTIFY_WEBHOOK # optional, any URL taking a JSON POST
npx wrangler secret put TELEGRAM_BOT_TOKEN # optional, see Notifications below
npx wrangler secret put TELEGRAM_CHAT_ID
npm run deploy3. Conecta tu cuenta
Abre https://your-worker.example.com/auth/start?key=<SETUP_KEY> en un navegador, aprueba una vez, listo. La página te indica la fecha de caducidad.
SETUP_KEY no es un adorno. Sin él, cualquiera que encontrara la URL podría vincular su cuenta de LinkedIn a tu Worker y sobrescribir tu token almacenado.
Usarlo desde Claude Code
claude mcp add --transport http linkedin https://your-worker.example.com/mcp \
--header "Authorization: Bearer $API_TOKEN"Tres herramientas:
Herramienta | Qué hace |
| Pasa el linter y luego publica. Rechaza si hay errores salvo que |
| Pasa el linter sin publicar. Úsalo mientras redactas. |
| En nombre de quién publica y cuántos días le quedan al token. |
Luego, en una sesión: "pasa este borrador por el linter y luego publícalo".
Usarlo desde un shell
# dry run: lint only, publish nothing
curl -s -X POST https://your-worker.example.com/post \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"…","dry_run":true}'
# publish
curl -s -X POST https://your-worker.example.com/post \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"…","visibility":"PUBLIC"}'
# token state
curl -s https://your-worker.example.com/status -H "Authorization: Bearer $API_TOKEN"/post devuelve 201 con el URN de la publicación y su URL pública, 422 cuando el linter lo bloquea, 503 cuando el token falta o ha caducado.
Notificaciones
Sin esto, el servicio falla como falla todo servicio desatendido: en silencio, y te enteras el día en que una publicación no sale. Un grupo de Telegram lo soluciona por el precio de un bot.
# 1. @BotFather in Telegram, /newbot, copy the token
npx wrangler secret put TELEGRAM_BOT_TOKEN
# 2. make a group, add the bot, send /start@yourbot in it
# (a bot with privacy mode on sees only messages that mention it)
# 3. read the group id back, then store it
open "https://your-worker.example.com/telegram/chatid?key=<SETUP_KEY>"
npx wrangler secret put TELEGRAM_CHAT_ID
# 4. prove it works
open "https://your-worker.example.com/telegram/test?key=<SETUP_KEY>"Qué llega:
Evento | Cuándo |
Resumen diario | en cada ejecución de cron, incluidos los días sin actividad |
Alarma de renovación | desde 14 días antes, encima del resumen de ese día |
Reconectado | se almacenó un token nuevo, con la nueva fecha de caducidad |
Publicado | cada publicación publicada, con su enlace |
Bloqueado | el linter rechazó un borrador, con las reglas que se activaron |
Rechazado | LinkedIn rechazó una publicación, con una suposición del motivo |
Sin token utilizable | un intento de publicación encontró el token ausente o caducado |
Cuota | el día supera 120 de las 150 solicitudes que LinkedIn permite a un miembro |
Dos decisiones de diseño que conviene conocer antes de cambiar nada:
El resumen diario es el latido. Se envía incluso cuando no ha pasado nada, de modo que su ausencia es en sí misma la alarma y nada tiene que vigilar al vigilante. Por eso también la alarma de renovación viaja encima del resumen en lugar de llegar como un segundo mensaje: exactamente un mensaje cada mañana, o te acostumbras a hojearlos.
El enlace de renovación lleva un nonce de un solo uso, no la clave de configuración. Un enlace con SETUP_KEY dejaría un secreto de larga duración en un registro de chat para siempre. El cron genera un nonce en su lugar, válido durante siete días, reemplazado por el siguiente recordatorio y eliminado en el momento en que se almacena un token. Las vistas previas de enlaces están desactivadas en todos los mensajes para que Telegram no pueda gastar el nonce al precargarlo.
Si no configuras nada, el servicio se comporta exactamente como antes: NOTIFY_WEBHOOK si lo tienes, el registro si no.
GET /telegram/preview?key=<SETUP_KEY>&kind=daily renderiza cualquier mensaje sin enviarlo, que es como se edita la redacción sin spamear al grupo. Añade &send=1 para verlo en el teléfono.
El transporte de la Bot API se prueba contra un stub local, incluida la migración de supergrupo que rompe silenciosamente los bots de Telegram meses después de la configuración:
npm run test:telegramEl linter
Los errores bloquean la publicación; las advertencias no.
Regla | Gravedad | Por qué |
| error | La señal más clara de prosa generada. Una coma o un punto siempre funcionan. |
| error | LinkedIn rechaza comentarios de más de 3000 caracteres. |
| error | Obvio, pero fácil de alcanzar al canalizar desde un archivo. |
| advertencia | LinkedIn no renderiza nada de eso. Los asteriscos y las almohadillas aparecen literalmente. |
Los guiones nunca se marcan. read-only tier, two-day sprint y per-client limits son correctos y deben permanecer. Solo U+2014 y U+2013 son errores.
Editar src/lint.ts es la forma prevista de hacerlo tuyo. Las reglas codifican las convenciones de escritura de una persona, no un estándar universal.
Cosas que te sorprenderán
La API de Posts no extrae URLs, pero LinkedIn igualmente las convierte en enlaces. Verificado en una publicación real: una URL desnuda se acorta a
lnkd.in/...y es clicable. Lo que no obtienes es la tarjeta de vista previa con imagen y título. Para eso necesitas una miniatura subida a través de la API de Images y un bloquecontent.article. Aún no implementado aquí.LinkedIn-Versiones obligatorio y las versiones se retiran.202508se apagó el 17 de agosto de 2026. Trata el valor enwrangler.jsonccomo un elemento de mantenimiento./v2/ugcPostsestá obsoleto. Esto usa/rest/posts, que lo reemplazó.Cambiar los scopes solicitados invalida todos los tokens existentes. Tendrás que volver a autorizar.
150 solicitudes por miembro al día. Por miembro, no por aplicación.
Estructura
Archivo | Contenido |
| Rutas, flujo OAuth, recordatorio cron |
| Llamadas a la API de OAuth y Posts, todos los endpoints en un solo lugar |
| Servidor MCP, JSON-RPC hecho a mano, sin estado |
| El linter de voz |
| Registro de tokens en KV, cálculos de caducidad, comparación de secretos a prueba de temporización |
| Qué eventos llegan al grupo y con qué palabras |
| Transporte de la Bot API, reintentos, migración de supergrupo |
| Los contadores del día, que el resumen lee de vuelta |
Desarrollo local
cp .dev.vars.example .dev.vars # fill in, never commit
npm run types # regenerate Env after editing wrangler.jsonc
npm run typecheck
npm run devwrangler dev --local te da un KV local, de modo que todo el flujo, salvo las llamadas reales a LinkedIn, se puede probar sin credenciales.
Notas de una configuración real
docs/2026-08-27-postavljanje-i-zamke.md documenta lo que costó realmente poner esto en marcha desde cero: el portal rechazando cualquier nombre de aplicación que contuviera "LinkedIn", la asociación irreversible de la Page, los scopes de OAuth que permanecen vacíos hasta que refrescas la página, %20 frente a + en el parámetro de scope, una caché DNS negativa que hizo parecer muerto un despliegue que funcionaba, y cómo se gestionaron los secretos para que ninguno pasara por una terminal.
docs/2026-08-27-telegram-obavijesti.md cubre la capa de notificaciones: la migración de supergrupo que rompe los bots de Telegram en silencio, por qué el latido es un resumen en lugar de un ping, y por qué el enlace de renovación lleva un nonce.
Licencia
MIT. Las reglas en src/lint.ts son mías; el mecanismo es tuyo para llevártelo.
This server cannot be deployed
Maintenance
Related MCP Connectors
Schedule and publish LinkedIn posts to profiles and company pages from AI agents.
Draft LinkedIn posts in your voice, then schedule or publish them through LinkedIn's official API.
Write LinkedIn posts in your own voice. Drafts only, ego never publishes for you.
Draft, reshape and schedule LinkedIn posts in the writer's own voice, not a generic AI one.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI agents to publish posts, images, comments, and reactions to LinkedIn as the authenticated user, with built-in safety features like daily budgets and deduplication.912 npmApache 2.0
- AlicenseAqualityBmaintenanceEnables AI agents to publish posts, images, videos, and articles to LinkedIn.10MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to draft, format, and manage LinkedIn posts through a review pipeline, with human approval via a web UI before publication.-
- FlicenseNot gradedqualityBmaintenanceEnables AI assistants to publish public text posts to an authenticated LinkedIn profile via the Model Context Protocol.-