Skip to main content
Glama
stepanic

linkedin-poster

by stepanic

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)

  1. 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.

  2. Crea la aplicación en linkedin.com/developers. Anota el ID de cliente y el Secreto de cliente.

  3. En la pestaña Productos, solicita ambos (son permisos abiertos y se aprueban al instante):

    • Sign In with LinkedIn using OpenID Connect → openid, profile

    • Share on LinkedIn → w_member_social

  4. En la pestaña Auth → añade la URL de redirección. Debe ser HTTPS, absoluta y coincidir con REDIRECT_URI cará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 deploy

3. 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

linkedin_post

Pasa el linter y luego publica. Rechaza si hay errores salvo que force: true.

linkedin_check

Pasa el linter sin publicar. Úsalo mientras redactas.

linkedin_status

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:telegram

El linter

Los errores bloquean la publicación; las advertencias no.

Regla

Gravedad

Por qué

no-em-dash, no-en-dash

error

La señal más clara de prosa generada. Una coma o un punto siempre funcionan.

max-length

error

LinkedIn rechaza comentarios de más de 3000 caracteres.

empty

error

Obvio, pero fácil de alcanzar al canalizar desde un archivo.

no-markdown-bold, no-markdown-link, no-markdown-heading, markdown-bullets

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 bloque content.article. Aún no implementado aquí.

  • LinkedIn-Version es obligatorio y las versiones se retiran. 202508 se apagó el 17 de agosto de 2026. Trata el valor en wrangler.jsonc como un elemento de mantenimiento.

  • /v2/ugcPosts está 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

src/index.ts

Rutas, flujo OAuth, recordatorio cron

src/linkedin.ts

Llamadas a la API de OAuth y Posts, todos los endpoints en un solo lugar

src/mcp.ts

Servidor MCP, JSON-RPC hecho a mano, sin estado

src/lint.ts

El linter de voz

src/tokens.ts

Registro de tokens en KV, cálculos de caducidad, comparación de secretos a prueba de temporización

src/notify.ts

Qué eventos llegan al grupo y con qué palabras

src/telegram.ts

Transporte de la Bot API, reintentos, migración de supergrupo

src/stats.ts

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 dev

wrangler 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.

Related MCP Connectors

Related MCP Servers