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 reminderWhat 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.
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. It counts the days down and pings you at 14, 7, 3 and 1 days out, while silent renewal still works.
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
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.
The 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. A link in your text stays a bare link. A real preview card needs a thumbnail uploaded through the Images API plus a
content.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 |
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.
Licence
MIT. The rules in src/lint.ts are mine; the mechanism is yours to take.
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
Give AI agents the LinkedIn tools to find, qualify, engage, and follow up with prospects.
Live LinkedIn data for AI agents: profiles, companies, jobs, posts, email finding. No account risk.
Managed LinkedIn MCP server for AI agents: search, connect, message and enrich on accounts you own.
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