LinkedIn Poster MCP — Publish to LinkedIn via natural language.
Post to LinkedIn without leaving the chat.
A Model Context Protocol server that turns any MCP client into a LinkedIn publisher — text posts, image posts with alt text, profile lookups, and token-health checks, all driven by natural language instead of the LinkedIn API console.
› Post the launch photo to LinkedIn, caption it about our v2 release.
// MCP tool call resolved automatically
{
"tool": "create_image_post",
"caption": "Shipping v2 today",
"images": ["launch.png"]
}
// polling upload status… AVAILABLE
ok post created — urn:li:share:7183…
| Never a blank image | Two transports | Admin dashboard | Bearer-guarded endpoint |
Polls LinkedIn's Images API until status is AVAILABLE before the post is created. |
Streamable HTTP on Vercel for production, plus a bundled stdio entry for local clients. | Optional panel surfacing auth status and tool call logs at a glance. | Every call to /api/mcp requires a bearer token before protocol handling begins. |
Four tools, one bearer token. Every tool call authenticates through the same Redis-backed token — no per-call re-auth.
| Tool | Input | Returns |
|---|---|---|
create_post |
text, visibility? |
created post ID |
create_image_post |
caption, images[1..20], visibility? |
created post ID |
get_profile |
— | name + email |
check_auth_status |
— | token validity + expiry |
Stateless endpoint, stateful token.
api/
authorize.ts GET start LinkedIn OAuth (CSRF state in Redis)
callback.ts GET exchange code for tokens, store, success page
mcp.ts POST MCP endpoint, bearer-auth, tools
dashboard.ts GET admin dashboard API
frontend/ React + Tailwind + MUI dashboard source -> public/
lib/
config.ts env vars, URLs, Redis client
linkedin-auth.ts token exchange, refresh, access-token helper
linkedin-api.ts profile, create_post, create_image_post
logging.ts structured logs to Redis
errors.ts NotAuthorizedError, LinkedInError
mcp-stdio.mjs esbuild-bundled stdio entry
Facts worth remembering:
- Tokens live under a single Redis key,
linkedin:tokens. /api/mcpis stateless and requiresAuthorization: Bearer <MCP_AUTH_TOKEN>on every request.- All LinkedIn calls use raw
fetch()— no LinkedIn SDK dependency.
One authorization, sixty days of posting.
Client -> /api/authorize -> LinkedIn OAuth -> /callback -> Redis -> MCP tools
| Step | What happens |
|---|---|
| 01 | /api/authorize creates a CSRF state, stores it in Redis for 10 minutes, redirects to LinkedIn. |
| 02 | LinkedIn authenticates the user and redirects back to /api/callback?code=...&state=.... |
| 03 | /api/callback validates state, exchanges the code, saves tokens to Redis. |
| 04 | Tools call getValidAccessToken() — refreshing near expiry, or throwing NotAuthorizedError. |
Note: LinkedIn issues a
refresh_tokenonly for apps provisioned with programmatic refresh. On the standard scope set, access tokens last 60 days and the user re-authorizes on expiry —check_auth_statusalways reports the real state.
Why posts never render blank.
- Register — the server registers the upload and receives a signed
uploadUrl. - Upload — image bytes are uploaded directly to that signed URL.
- Poll -> post — status is polled until
AVAILABLE, then — and only then — the post is created.
- Node.js 24+
- LinkedIn Developer app with approved scopes:
openid profile email w_member_social - An Upstash Redis database
Set these in Vercel and in a root .env for local development:
| Variable | Purpose |
|---|---|
LINKEDIN_CLIENT_ID |
LinkedIn app client ID |
LINKEDIN_CLIENT_SECRET |
LinkedIn app client secret |
LINKEDIN_REDIRECT_URI |
Must match a whitelisted redirect URL exactly |
UPSTASH_REDIS_REST_URL |
Upstash Redis REST URL |
UPSTASH_REDIS_REST_TOKEN |
Upstash Redis REST token |
MCP_AUTH_TOKEN |
Bearer token MCP clients send to /api/mcp |
These files are git-ignored — never commit secrets.
npm install
vercel dev # http://localhost:3000/api/...
npm run typecheck # tsc --noEmitlocal http://localhost:3000/api/callback
prod https://<your-app>.vercel.app/api/callback
Whitelist the exact URL in the LinkedIn Developer Portal — LinkedIn matches redirects with no tolerance for query params.
Remote or local — pick one.
Remote — Streamable HTTP
- Type: Custom / MCP server, Streamable HTTP
- Endpoint:
https://<your-deployed-url>/api/mcp - Header:
Authorization: Bearer <MCP_AUTH_TOKEN>
Local — stdio
Point the client at the bundled entry:
node /path/to/linkedin-post-mcp/mcp-stdio.mjs
Ship it to Vercel.
vercel # preview
vercel --prod # productionvercel.jsonpinsapi/**to the Node.js runtime.- Set the production callback URL for LinkedIn.
- After deploying, open
/api/authorizeonce — then every tool works.
What's actually guarded.
- Secrets come from environment variables only — nothing is logged.
- Endpoint — the MCP endpoint requires a bearer token before protocol handling.
- CSRF — OAuth
stateguards against request forgery and is one-time-use with a short TTL.
Node.js 24+ · Streamable HTTP + stdio · Upstash Redis · LinkedIn OAuth2