Skip to main content
Glama
Selangwe

linkedin-mcp-server

by Selangwe

linkedin-mcp-server

An MCP server for working a personal LinkedIn profile: publishing document ("carousel") posts, commenting, and running prospect follow-up — plus an honest account of what LinkedIn's API will and won't let a self-serve app do.

It also hosts the LinkedIn OAuth flow itself, so you don't need a separate public server just to catch the redirect — this one is that server.

What LinkedIn actually allows

Read this before planning a workflow around it. Most of the interesting capabilities are gated, and the gate is LinkedIn's access model, not this code.

Capability

Status

Why

Publish posts / carousels

Works

w_member_social, self-serve "Share on LinkedIn"

Read own profile

Works

OpenID Connect profile scope

Comment / reply to a comment

Probably — probe it

The permission table lists w_member_social, but the endpoint sits under the Community Management API and may need that product too. Run linkedin_capabilities with probe: "safe" to classify it without posting anything.

Read comments on your posts

No

Needs r_member_social, a closed permission: "access requests are not being accepted at this time". So comment URNs must be pasted in from the browser.

Post/profile analytics

No

memberCreatorPostAnalytics needs r_member_postAnalytics, granted only via the Community Management API — "registered legal organizations for commercial use cases only".

Send / read DMs

No

The Messages API is restricted to approved partners, and partners are separately barred from automated or scheduled sends.

People search / prospecting

No

LinkedIn exposes no people-search endpoint at any tier. The Connections API is restricted and first-degree only.

Two consequences worth internalising:

  • Outreach works by drafting, not sending. linkedin_outreach_run renders the next message and hands it to you to send, then tracks it once you confirm. That is the normal, successful path — not a failure mode.

  • There is no scheduler, deliberately. Follow-ups are pull-based (linkedin_outreach_due), because LinkedIn forbids automated sends even for approved partners.

Anything marked "No" above is only reachable through an unofficial provider you run yourself. That is off by default — see Unofficial provider.

Related MCP server: LinkedIn Post MCP

Tools

Posting

Tool

What it does

linkedin_upload_document

Uploads a PDF (from a URL) to LinkedIn as a document asset. Publishes nothing.

linkedin_create_post

Publishes a post referencing an uploaded document. Two-phase, irreversible.

linkedin_post_carousel

Download → upload → publish in one call. Two-phase, irreversible.

linkedin_comment_reply

Comments on a post, or replies to a comment. Target is a pasted URL or URN.

Outreach

Tool

What it does

linkedin_prospect_add / _import / _update

Maintain the local prospect list. Marking someone replied stops their sequence.

linkedin_sequence_define

Define a multi-step follow-up sequence with templates and delays.

linkedin_outreach_enroll

Start a prospect on a sequence.

linkedin_outreach_due

What follow-up is due now.

linkedin_outreach_run

Render the next step — sends it if that is possible, otherwise drafts it.

linkedin_outreach_mark_sent

Confirm a drafted step went out; advances the sequence.

Diagnostics and safety

Tool

What it does

linkedin_auth_status

Token expiry, granted scopes, re-authorization deadline. No API call.

linkedin_get_profile

Confirms which account is authenticated.

linkedin_capabilities

Per-capability verdict, why, and what to use instead. Optional live probing.

linkedin_post_history

Posts published through this server.

linkedin_analytics_summary

Real metrics when the scope exists; local cadence and an honest reason when it doesn't.

linkedin_kill_switch

Halt or resume every outward action.

linkedin_audit_log

What was done and what was refused.

linkedin_send_message appears only when an unofficial provider is configured.

Safety model

The target is normally the operator's main LinkedIn account, so the defaults are conservative and the rails sit in the tool layer — where they also cover the unofficial provider, which bypasses the HTTP client entirely.

  • Two-phase confirmation. Every outward action previews first and returns a single-use token bound to a digest of the exact payload. Editing the text invalidates the token, so what you approved is what goes out. It is not a confirm: true boolean, because a model would simply set one.

  • Daily caps (15 messages, 20 comments, 3 posts, 40 total) and a 90s minimum gap between paced actions. The throttle refuses with a retry time rather than sleeping.

  • Kill switch, as a tool, an HTTP route (POST /admin/kill-switch, so you can hit it from a phone), and an env var that cannot be cleared from a tool.

  • Audit log of every action including refusals, storing message digests rather than message bodies.

All tunable — see .env.example.

Unofficial provider

LinkedIn licenses no third party to send member-to-member DMs or search people; services that appear to do so drive LinkedIn's internal endpoints with a session cookie, which breaks the User Agreement and can get an account restricted.

So this repo ships the seam, not the mechanism: set LINKEDIN_PROVIDER_URL to an endpoint you run, and requests are forwarded there HMAC-signed. Enabling it needs two variables — LINKEDIN_UNOFFICIAL_PROVIDER=http and LINKEDIN_UNOFFICIAL_ACK=i-accept-tos-risk — so a copied .env cannot switch it on by accident. With it off, the capability report says so and nothing changes.

Development

npm install
npm test          # unit tests, no credentials needed
npm run typecheck # includes api/, which the build's tsconfig excludes
npm run build

1. Create the LinkedIn app (one-time, ~5 minutes)

  1. Go to https://www.linkedin.com/developers/apps → Create app.

  2. Fill in the required fields. LinkedIn requires the app be associated with a Company Page even for personal-profile posting — if you don't have one, create a minimal one for this purpose.

  3. On the Products tab, request "Share on LinkedIn". This is self-serve for the scopes this server needs (openid, profile, w_member_social) — no manual review wait.

  4. On the Auth tab, note the Client ID and Client Secret, and add an Authorized redirect URL of:

    https://<your-deployed-host>/oauth/linkedin/callback

    (must match LINKEDIN_REDIRECT_URI exactly, including scheme/host/path)

2. Deploy the server

Two supported paths — pick based on where you want to host this.

Option A: Vercel (serverless)

Vercel Functions have no persistent disk, so the file-based token store won't survive between requests. Use the built-in Redis-backed store instead:

  1. Push this repo to GitHub, then import it in Vercel (New Project → your repo). It's picked up automatically — api/index.ts is the serverless entrypoint, vercel.json routes everything there.

  2. In the Vercel project, go to Storage → Marketplace Database Providers → Upstash → Redis and create one. This injects Redis env vars into your project automatically.

  3. Add the rest of the environment variables (Project Settings → Environment Variables):

    LINKEDIN_CLIENT_ID=...
    LINKEDIN_CLIENT_SECRET=...
    LINKEDIN_REDIRECT_URI=https://<your-vercel-domain>/oauth/linkedin/callback
    TOKEN_STORE_DRIVER=kv
    MCP_AUTH_TOKEN=<a long random string>
  4. Redeploy so the new env vars take effect.

The 4.5 MB request-body limit on Vercel Functions doesn't affect this server — the PDF is fetched by an outbound request from linkedin_post_carousel (you pass a pdf_url, not the file itself), not received as an inbound upload. The 300s default duration on every plan is comfortably more than an upload+publish needs.

Option B: A host with a real disk (Fly.io, Railway, a VPS)

npm install
npm run build
npm start   # or: node dist/index.js

Set these environment variables (see .env.example):

LINKEDIN_CLIENT_ID=...
LINKEDIN_CLIENT_SECRET=...
LINKEDIN_REDIRECT_URI=https://<your-deployed-host>/oauth/linkedin/callback
TOKEN_STORE_DRIVER=file
TOKEN_STORE_PATH=./data/tokens.json   # point this at a persistent volume
MCP_AUTH_TOKEN=<a long random string> # protects /mcp and /oauth/linkedin/start
PORT=3000
TRANSPORT=http

Either way, you need a public HTTPS URL (for the OAuth redirect and for Claude to reach /mcp).

3. Authorize (one-time human step)

Open, in a browser where you're logged into the LinkedIn account you want to post as:

https://<your-deployed-host>/oauth/linkedin/start?token=<MCP_AUTH_TOKEN>

Click Allow. You'll land back on /oauth/linkedin/callback, which exchanges the code for tokens and saves them to TOKEN_STORE_PATH. That page tells you exactly when this session expires — read it, because the answer depends on your LinkedIn app.

Token lifetime & re-auth

There are two cases, and they behave very differently:

  • Your app is approved for programmatic refresh (LinkedIn returned a refresh token). The server refreshes the access token by itself, ahead of expiry, and retries once if LinkedIn rejects a token mid-call. You still have to repeat step 3 roughly once a year — refresh tokens last ~365 days and LinkedIn does not extend that deadline when it issues a new access token.

  • It isn't (no refresh token returned). Nothing renews. Everything works for ~60 days and then every tool call fails until you repeat step 3. The callback page warns you about this explicitly, as does every successful tool result once the deadline gets close.

Check where you stand at any time — neither of these makes a LinkedIn API call:

curl -H "Authorization: Bearer $MCP_AUTH_TOKEN" https://<your-host>/auth/status

or ask the model to call the linkedin_auth_status tool. Both report hard_deadline_at — the date by which a human must re-authorize — plus a warning string that is only present when action is actually needed.

Successful tool results carry that warning as an extra content block starting within 14 days of the deadline (tune with LINKEDIN_AUTH_WARN_DAYS), so a looming expiry surfaces while you're working rather than as a surprise failure.

4. Register as a connector

This server hosts its own minimal OAuth 2.1 authorization server (/.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server, /register, /authorize, /token — RFC9728, RFC8414, RFC7591, RFC8707, PKCE), so it can be added as a real custom connector wherever a client only supports "no auth" or full OAuth rather than a static bearer header (e.g. Claude's Settings → Connectors → Add custom connector UI):

  1. Add https://<your-deployed-host>/mcp as a custom connector's URL and choose OAuth. Most clients handle Dynamic Client Registration and PKCE automatically from there — no Client ID/Secret to paste in.

  2. When the client opens the authorization page in a browser, you'll be asked to enter MCP_AUTH_TOKEN. That's the entire "login" step — this server is single-user, so proving you know that value is the consent gate. Submit it and you'll be redirected back with the connector connected.

  3. Access tokens this layer issues are short-lived (1 hour) and refreshed automatically by the client using the refresh token from step 2 — no further action needed under normal use.

If your client only supports a static bearer token (e.g. MCP Inspector, or curl), you can skip all of the above and just send Authorization: Bearer <MCP_AUTH_TOKEN> directly — the old path still works unchanged and is treated as a master key by the same /mcp route.

Local development

npm run dev   # tsx watch, auto-reloads on save

For local testing you can run with TRANSPORT=stdio and connect via any stdio-based MCP client (e.g. npx @modelcontextprotocol/inspector), though you'll still need LINKEDIN_REDIRECT_URI reachable from your browser to complete the OAuth step — a tunnel like ngrok works well for this during development.

Notes / limitations

  • Single-user by design (one LinkedIn account, one token file). Don't expose this server publicly without MCP_AUTH_TOKEN set — it protects /mcp, /oauth/linkedin/start, and doubles as the consent gate on the connector OAuth server's /authorize page.

  • The connector-facing OAuth layer (/register, /authorize, /token) is intentionally separate from the LinkedIn-facing OAuth flow (/oauth/linkedin/start / /callback) — the former lets clients (like Claude) authenticate to this server; the latter lets this server authenticate to LinkedIn. Don't confuse the two code/token exchanges if you're debugging.

  • Registered OAuth clients, authorization codes, and issued access/refresh tokens are stored the same way as the LinkedIn token (file or Redis, via TOKEN_STORE_DRIVER) — see src/services/kv-store.ts / src/services/oauth-store.ts. Access tokens expire in 1 hour; refresh tokens don't expire but aren't rotated, matching this server's single-operator threat model. If you ever need to revoke everything, clear the data/oauth-* files (file driver) or flush those key prefixes in Redis (kv driver).

  • Set PUBLIC_BASE_URL (e.g. https://your-app.vercel.app) if you're behind a proxy that doesn't set X-Forwarded-Proto/Host correctly — the OAuth metadata endpoints otherwise infer the base URL from the incoming request.

  • LinkedIn posts published via linkedin_create_post / linkedin_post_carousel cannot be edited or deleted through this API — only from the LinkedIn UI. There's no "undo" tool here on purpose; double-check the caption and PDF before calling.

  • LinkedIn's native ads-only "Carousel" format has no organic API equivalent; what these tools produce is a document post, which LinkedIn renders as a swipeable carousel in the feed — visually the same thing users mean by "LinkedIn carousel."

  • LINKEDIN_API_VERSION in src/constants.ts is a calendar-month version string LinkedIn requires on every REST call. Bump it periodically per LinkedIn's versioning docs.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    An MCP server for LinkedIn REST API v2 that enables AI assistants to create, list, and delete posts, manage events, upload images, comment, and react—featuring OAuth 2.0 with session persistence, local post history tracking, and multiple automated tests
    74 npm
    10
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that lets you publish posts to your LinkedIn profile using the official LinkedIn API, with support for markdown formatting and OAuth authentication.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server that posts to LinkedIn through the LinkedIn API. Enables creating text posts on your LinkedIn profile using natural language from any MCP-compatible client.
    3
    ISC
  • F
    license
    Not graded
    quality
    C
    maintenance
    A production-ready MCP server for interacting with the LinkedIn API, enabling profile lookup, post creation and deletion, image posting, and organization page management through a secure tool-based interface.
    -