linkedin-poster
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@linkedin-postercheck this draft, then post it to my feed"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
linkedin-poster
Post to your own LinkedIn feed from Claude Code, a shell, or a cron job. One Cloudflare Worker, no dependencies, three surfaces: an MCP server so an agent can publish directly, an HTTP API for scripts, and a voice linter that refuses to publish text carrying the tells of machine-written prose.
Built because a good post is worth automating and a bad em-dash is not worth shipping. This README has no em-dashes in it either, which is the point.
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)What it is not
This posts as one member: you. It is not a scheduling SaaS, not a multi-tenant tool, and it does not touch anybody else's account. Everything it needs is self-serve; no LinkedIn partner program is involved.
Related MCP server: LinkedIn MCP Server
The constraint that shapes everything
LinkedIn access tokens last 60 days, and programmatic refresh tokens are limited to approved Marketing Developer Platform partners. A self-serve app never gets one. There is no way around this, and any guide promising set-and-forget LinkedIn automation is wrong.
What there is: while your current token is still valid and you are logged into LinkedIn, hitting the authorization endpoint again skips the consent screen and just redirects. So renewal is one click every seven weeks, as long as you do it before the token dies. Miss the window and you get the consent screen back, which is two clicks instead of one.
That is why this service ships with a cron trigger. From 14 days out it puts a renewal alarm at the top of the daily summary, every morning, while silent renewal still works. See Notifications.
Setup
1. LinkedIn side (manual, about ten minutes)
You need a LinkedIn Page. The developer portal makes you attach the app to one and have a page admin verify it.
Create the app at linkedin.com/developers. Note the Client ID and Client Secret.
Products tab, request both (they are Open Permissions and approve instantly):
Sign In with LinkedIn using OpenID Connect →
openid,profileShare on LinkedIn →
w_member_social
Auth tab → add the redirect URL. It must be HTTPS, absolute, and match
REDIRECT_URIcharacter for character:https://your-worker.example.com/auth/callback
2. Worker side
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. Connect your account
Open https://your-worker.example.com/auth/start?key=<SETUP_KEY> in a browser,
approve once, done. The page tells you the expiry date.
SETUP_KEY is not decoration. Without it, anyone who found the URL could bind
their LinkedIn account to your Worker and overwrite your stored token.
Using it from Claude Code
claude mcp add --transport http linkedin https://your-worker.example.com/mcp \
--header "Authorization: Bearer $API_TOKEN"Three tools:
Tool | What it does |
| Lints, then publishes. Refuses on errors unless |
| Lints without publishing. Use it while drafting. |
| Who it posts as, and how many days the token has left. |
Then, in a session: "check this draft against the linter, then post it."
Using it from a 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 returns 201 with the post URN and its public URL, 422 when the linter
blocks it, 503 when the token is missing or dead.
Notifications
Without this the service fails the way every unattended service fails: quietly, and you find out on the day a post does not go out. A Telegram group fixes that for the price of one 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>"What arrives:
Event | When |
Daily summary | every cron run, quiet days included |
Renewal alarm | from 14 days out, on top of that day's summary |
Reconnected | a fresh token was stored, with the new expiry date |
Posted | every published post, with its link |
Blocked | the linter refused a draft, with the rules that fired |
Rejected | LinkedIn refused a post, with a guess at why |
No usable token | a publish attempt found the token missing or dead |
Quota | the day crosses 120 of the 150 requests LinkedIn allows a member |
Two design decisions worth knowing before you change anything:
The daily summary is the heartbeat. It goes out even when nothing happened, so its absence is itself the alarm and nothing has to monitor the monitor. That is also why a renewal alarm rides on top of the summary rather than arriving as a second message: exactly one message a morning, or you train yourself to skim.
The renewal link carries a one-time nonce, not the setup key. A link with
SETUP_KEY in it would park a long-lived secret in a chat log for good. The
cron mints a nonce instead, valid for seven days, replaced by the next reminder,
and deleted the moment a token is stored. Link previews are disabled on every
message so Telegram cannot spend the nonce by prefetching it.
Set none of it and the service behaves exactly as it did before: NOTIFY_WEBHOOK
if you have one, the log if you do not.
GET /telegram/preview?key=<SETUP_KEY>&kind=daily renders any message without
sending it, which is how to edit the wording without spamming the group. Add
&send=1 to see it on a phone.
The Bot API transport is tested against a local stub, including the supergroup migration that silently breaks Telegram bots months after setup:
npm run test:telegramThe linter
Errors block publishing; warnings do not.
Rule | Severity | Why |
| error | The clearest tell of generated prose. A comma or a full stop always works. |
| error | LinkedIn rejects commentary over 3000 characters. |
| error | Obvious, but easy to hit when piping from a file. |
| warning | LinkedIn renders none of it. Asterisks and hash marks show up literally. |
Hyphens are never flagged. read-only tier, two-day sprint and
per-client limits are correct and must stay. Only U+2014 and U+2013 are errors.
Editing src/lint.ts is the intended way to make this yours. The rules encode
one person's writing conventions, not a universal standard.
Things that will surprise you
The Posts API does not scrape URLs, but LinkedIn still linkifies them. Verified on a live post: a bare URL is shortened to
lnkd.in/...and is clickable. What you do not get is the preview card with an image and title. For that you need a thumbnail uploaded through the Images API plus acontent.articleblock. Not implemented here yet.LinkedIn-Versionis mandatory and versions get sunset.202508went dark on 17 August 2026. Treat the value inwrangler.jsoncas a maintenance item./v2/ugcPostsis deprecated. This uses/rest/posts, which replaced it.Changing the requested scopes invalidates every existing token. You will have to re-authorize.
150 requests per member per day. Per member, not per app.
Layout
File | Contains |
| Routes, OAuth flow, cron reminder |
| OAuth and Posts API calls, all endpoints in one place |
| MCP server, hand-rolled JSON-RPC, stateless |
| The voice linter |
| KV token record, expiry maths, timing-safe secret compare |
| Which events reach the group, and in what words |
| Bot API transport, retries, supergroup migration |
| The day's counters, which the summary reads back |
Local development
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 gives you a local KV, so the whole flow short of the real
LinkedIn calls can be exercised without credentials.
Notes from a real setup
docs/2026-08-27-postavljanje-i-zamke.md
records what setting this up from zero actually cost: the portal rejecting any app
name containing "LinkedIn", the irreversible Page association, OAuth scopes that
stay empty until you refresh the page, %20 versus + in the scope parameter, a
negative DNS cache that made a working deployment look dead, and how secrets were
handled so none of them passed through a terminal.
docs/2026-08-27-telegram-obavijesti.md
covers the notification layer: the supergroup migration that breaks Telegram bots
in silence, why the heartbeat is a summary rather than a ping, and why the
renewal link carries a nonce.
Licence
MIT. The rules in src/lint.ts are mine; the mechanism is yours to take.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This 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 Connectors
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.
1Write LinkedIn posts in your voice: ideas, drafts, scheduling, analytics from your personal AI.
Give AI agents the LinkedIn tools to find, qualify, engage, and follow up with prospects.
Related MCP Servers
- AlicenseAqualityAmaintenanceEnables 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.929Apache 2.0
- AlicenseBqualityCmaintenanceEnables drafting, formatting, analyzing, and publishing LinkedIn posts directly from MCP-compatible clients like Claude.5214MIT
- 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.-
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/stepanic/linkedin-poster'
If you have feedback or need assistance with the MCP directory API, please join our Discord server