Skip to main content
Glama
stepanic

linkedin-poster

by stepanic

linkedin-poster

Poste in deinen eigenen LinkedIn-Feed aus Claude Code, einer Shell oder einem Cron-Job. Ein Cloudflare Worker, keine Abhängigkeiten, drei Oberflächen: ein MCP-Server, damit ein Agent direkt veröffentlichen kann, eine HTTP-API für Skripte und ein Stil-Linter, der sich weigert, Text zu veröffentlichen, der die Anzeichen maschinengeschriebener Prosa trägt.

Gebaut, weil ein guter Beitrag die Automatisierung wert ist und ein schlechter em-dash es nicht wert ist, veröffentlicht zu werden. Auch diese README enthält keine em-dashes, und genau das ist der Punkt.

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)

Was es nicht ist

Dies postet als ein Mitglied: du. Es ist kein Planungs-SaaS, kein Multi-Tenant-Tool und es berührt kein Konto anderer. Alles, was es braucht, ist Self-Service; kein LinkedIn-Partnerprogramm ist beteiligt.

Related MCP server: mcp-server-linkedin

Die Einschränkung, die alles prägt

LinkedIn-Zugriffstokens halten 60 Tage, und programmatische Refresh-Tokens sind auf zugelassene Marketing Developer Platform-Partner beschränkt. Eine Self-Service-App bekommt nie eines. Daran führt kein Weg vorbei, und jeder Leitfaden, der LinkedIn-Automatisierung mit Einrichten-und-Vergessen verspricht, liegt falsch.

Was es gibt: Solange dein aktuelles Token noch gültig ist und du bei LinkedIn angemeldet bist, überspringt ein erneuter Aufruf des Autorisierungs-Endpunkts den Zustimmungsbildschirm und leitet einfach weiter. Die Erneuerung ist also alle sieben Wochen ein Klick, solange du es tust, bevor das Token stirbt. Verpasst du das Zeitfenster, bekommst du den Zustimmungsbildschirm zurück, also zwei Klicks statt einem.

Deshalb wird dieser Dienst mit einem Cron-Trigger ausgeliefert. Ab 14 Tagen vor Ablauf setzt er jeden Morgen einen Erneuerungsalarm an den Anfang der Tageszusammenfassung, solange die stille Erneuerung noch funktioniert. Siehe Benachrichtigungen.

Einrichtung

1. LinkedIn-Seite (manuell, etwa zehn Minuten)

  1. Du brauchst eine LinkedIn-Seite. Das Entwicklerportal verlangt, dass du die App an eine Seite anhängst und sie von einem Seiten-Admin verifizieren lässt.

  2. Erstelle die App unter linkedin.com/developers. Notiere dir die Client ID und das Client Secret.

  3. Tab Produkte, fordere beide an (es sind offene Berechtigungen und sie werden sofort genehmigt):

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

    • Share on LinkedIn → w_member_social

  4. Tab Auth → füge die Redirect-URL hinzu. Sie muss HTTPS, absolut und zeichengenau mit REDIRECT_URI übereinstimmen: https://your-worker.example.com/auth/callback

2. Worker-Seite

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. Verbinde dein Konto

Öffne https://your-worker.example.com/auth/start?key=<SETUP_KEY> in einem Browser, genehmige einmal, fertig. Die Seite zeigt dir das Ablaufdatum.

SETUP_KEY ist keine Deko. Ohne ihn könnte jeder, der die URL findet, sein LinkedIn-Konto an deinen Worker binden und dein gespeichertes Token überschreiben.

Verwendung mit Claude Code

claude mcp add --transport http linkedin https://your-worker.example.com/mcp \
  --header "Authorization: Bearer $API_TOKEN"

Drei Tools:

Tool

Funktion

linkedin_post

Lintet, dann veröffentlicht. Lehnt bei Fehlern ab, außer mit force: true.

linkedin_check

Lintet ohne zu veröffentlichen. Zum Entwerfen verwenden.

linkedin_status

Als wessen Konto es postet und wie viele Tage das Token noch hat.

Dann, in einer Sitzung: „Prüfe diesen Entwurf mit dem Linter und veröffentliche ihn dann.“

Verwendung aus der 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 gibt 201 mit der Post-URN und der öffentlichen URL zurück, 422, wenn der Linter es blockiert, 503, wenn das Token fehlt oder abgelaufen ist.

Benachrichtigungen

Ohne das scheitert der Dienst so, wie jeder unbeaufsichtigte Dienst scheitert: leise, und du erfährst es an dem Tag, an dem ein Beitrag nicht erscheint. Eine Telegram-Gruppe behebt das für den Preis eines Bots.

# 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>"

Was ankommt:

Ereignis

Wann

Tägliche Zusammenfassung

bei jedem Cron-Lauf, auch an ruhigen Tagen

Erneuerungsalarm

ab 14 Tagen vor Ablauf, zusätzlich zur Zusammenfassung des Tages

Wiederverbunden

ein neues Token wurde gespeichert, mit dem neuen Ablaufdatum

Veröffentlicht

jeder veröffentlichte Beitrag, mit seinem Link

Blockiert

der Linter hat einen Entwurf abgelehnt, mit den ausgelösten Regeln

Abgelehnt

LinkedIn hat einen Beitrag abgelehnt, mit einer Vermutung warum

Kein brauchbares Token

ein Veröffentlichungsversuch fand das Token fehlend oder abgelaufen

Kontingent

der Tag überschreitet 120 der 150 Anfragen, die LinkedIn einem Mitglied erlaubt

Zwei Designentscheidungen, die du kennen solltest, bevor du etwas änderst:

Die tägliche Zusammenfassung ist der Herzschlag. Sie geht auch raus, wenn nichts passiert ist, sodass ihre Abwesenheit selbst der Alarm ist und nichts den Monitor überwachen muss. Genau deshalb reitet ein Erneuerungsalarm auf der Zusammenfassung, statt als zweite Nachricht zu kommen: genau eine Nachricht pro Morgen, sonst trainierst du dich darauf, nur zu überfliegen.

Der Erneuerungslink trägt ein Einmal-Nonce, nicht den Setup-Schlüssel. Ein Link mit SETUP_KEY würde ein langlebiges Geheimnis dauerhaft in einem Chatprotokoll parken. Der Cron prägt stattdessen ein Nonce, sieben Tage gültig, ersetzt durch die nächste Erinnerung und gelöscht in dem Moment, in dem ein Token gespeichert wird. Linkvorschauen sind auf jeder Nachricht deaktiviert, damit Telegram das Nonce nicht durch Vorabruf verbraucht.

Stellst du nichts davon ein, verhält sich der Dienst genau wie zuvor: NOTIFY_WEBHOOK, falls du einen hast, sonst das Log.

GET /telegram/preview?key=<SETUP_KEY>&kind=daily rendert jede Nachricht, ohne sie zu senden. So lässt sich der Wortlaut bearbeiten, ohne die Gruppe zuzuspammen. Füge &send=1 hinzu, um sie auf einem Telefon zu sehen.

Der Bot-API-Transport wird gegen einen lokalen Stub getestet, einschließlich der Supergruppen-Migration, die Telegram-Bots Monate nach der Einrichtung stillschweigend zerstört:

npm run test:telegram

Der Linter

Fehler blockieren das Veröffentlichen; Warnungen nicht.

Regel

Schweregrad

Warum

no-em-dash, no-en-dash

Fehler

Das deutlichste Anzeichen generierter Prosa. Ein Komma oder ein Punkt funktioniert immer.

max-length

Fehler

LinkedIn lehnt Kommentare über 3000 Zeichen ab.

empty

Fehler

Offensichtlich, aber leicht zu treffen, wenn man aus einer Datei piped.

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

Warnung

LinkedIn rendert nichts davon. Sternchen und Rauten erscheinen wörtlich.

Bindestriche werden nie markiert. read-only tier, two-day sprint und per-client limits sind korrekt und müssen bleiben. Nur U+2014 und U+2013 sind Fehler.

src/lint.ts zu bearbeiten ist der vorgesehene Weg, das hier zu deinem zu machen. Die Regeln kodieren die Schreibkonventionen einer Person, keinen universellen Standard.

Dinge, die dich überraschen werden

  • Die Posts-API scrapt keine URLs, aber LinkedIn verlinkt sie trotzdem. An einem Live-Beitrag verifiziert: Eine nackte URL wird zu lnkd.in/... gekürzt und ist klickbar. Was du nicht bekommst, ist die Vorschaukarte mit Bild und Titel. Dafür brauchst du ein über die Images-API hochgeladenes Thumbnail plus einen content.article-Block. Hier noch nicht implementiert.

  • LinkedIn-Version ist Pflicht und Versionen werden eingestellt. 202508 wurde am 17. August 2026 abgeschaltet. Behandle den Wert in wrangler.jsonc als Wartungsposten.

  • /v2/ugcPosts ist veraltet. Dies verwendet /rest/posts, das es ersetzt hat.

  • Das Ändern der angeforderten Scopes macht jedes vorhandene Token ungültig. Du musst dich neu autorisieren.

  • 150 Anfragen pro Mitglied pro Tag. Pro Mitglied, nicht pro App.

Aufbau

Datei

Enthält

src/index.ts

Routen, OAuth-Ablauf, Cron-Erinnerung

src/linkedin.ts

OAuth- und Posts-API-Aufrufe, alle Endpunkte an einem Ort

src/mcp.ts

MCP-Server, handgeschriebenes JSON-RPC, zustandslos

src/lint.ts

Der Stil-Linter

src/tokens.ts

KV-Token-Datensatz, Ablaufberechnung, timing-sicherer Geheimnisvergleich

src/notify.ts

Welche Ereignisse die Gruppe erreichen und in welchen Worten

src/telegram.ts

Bot-API-Transport, Wiederholungen, Supergruppen-Migration

src/stats.ts

Die Zähler des Tages, die die Zusammenfassung ausliest

Lokale Entwicklung

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 gibt dir ein lokales KV, sodass der gesamte Ablauf bis auf die echten LinkedIn-Aufrufe ohne Anmeldedaten getestet werden kann.

Notizen aus einer echten Einrichtung

docs/2026-08-27-postavljanje-i-zamke.md hält fest, was die Einrichtung von Null an tatsächlich gekostet hat: das Portal, das jeden App-Namen mit „LinkedIn“ ablehnt, die irreversible Seiten-Zuordnung, OAuth-Scopes, die leer bleiben, bis du die Seite aktualisierst, %20 gegenüber + im Scope-Parameter, ein negativer DNS-Cache, der eine funktionierende Bereitstellung tot aussehen ließ, und wie mit Geheimnissen umgegangen wurde, sodass keines durch ein Terminal ging.

docs/2026-08-27-telegram-obavijesti.md behandelt die Benachrichtigungsebene: die Supergruppen-Migration, die Telegram-Bots stillschweigend zerstört, warum der Herzschlag eine Zusammenfassung und kein Ping ist, und warum der Erneuerungslink ein Nonce trägt.

Lizenz

MIT. Die Regeln in src/lint.ts sind meine; den Mechanismus kannst du übernehmen.

Related MCP Connectors

Related MCP Servers