Skip to main content
Glama

linkedin-mcp

An MCP (Model Context Protocol) server for LinkedIn and job hunting: authenticate via OAuth 2.0, read your profile, draft and analyze posts with an LLM, publish posts to your feed, search free job boards, and track applications — all through tools any MCP client (Claude, Cursor, custom agents) can call.

User / agent
      │  (MCP: stdio or streamable HTTP)
      ▼
 linkedin-mcp (FastMCP + FastAPI)
      │  ├── LinkedIn OAuth 2.0 (authorization code + refresh)
      │  ├── LinkedIn REST API  (profile, email, create post)
      │  ├── Job boards  (Remotive · Arbeitnow · RemoteOK · Jobicy — keyless)
      │  ├── Application tracker  (local JSON store)
      │  └── LLM provider layer (Grok · OpenAI · Ollama · unorouter — swappable via env)
      ▼
  LinkedIn API  /  job board APIs  /  your LLM provider

What's implemented (Phase 1 + a taste of Phase 3)

Tool

What it does

auth_status

Check OAuth state; returns the login URL or a clear "what's missing" message

get_profile

Member profile (name, picture, locale, email) via OpenID Connect /v2/userinfo

get_email

Member email (requires the email scope)

create_post

Publish a text or image post via the current Posts API (/rest/posts)

draft_post

Generate a LinkedIn post with the configured LLM provider

analyze_post

Expert critique + suggested rewrites of a draft via the LLM

search_jobs

Search Remotive/Arbeitnow/RemoteOK/Jobicy — free, keyless, no LinkedIn permissions

match_jobs_to_profile

LLM-scored fit for listings from search_jobs against your profile

prepare_application

One call → fit assessment + cover letter + recruiter DM + interview prep

track_application

Save an application locally (company, role, URL, status, notes)

list_applications

List tracked applications, filterable by status

update_application

Move an application along: saved → applied → interview → offer/rejected

followup_applications

Prioritized view of what needs action next

analyze_job_fit

Compare a job description against your profile: fit score, gaps, talking points

draft_application

Tailored cover letter or recruiter DM from a job description + your profile

draft_job_post

Open-to-work / job-search announcement post

schedule_post

Queue a post to be published automatically at a future time

list_scheduled_posts

List queued posts, filterable by status

cancel_scheduled_post

Remove a queued post before it publishes

publish_scheduled_post_now

Publish a queued post immediately (handy for testing)

Resource linkedin://profile

Profile exposed as an MCP resource

Job search & application tracking

LinkedIn offers no public jobs API, so search goes through four free keyless boards (verified live): Remotive, Arbeitnow, RemoteOK, and Jobicy. The typical agent flow:

search_jobs("python fastapi")  →  match_jobs_to_profile(listings)
       →  prepare_application(jd)  →  track_application(...)
       →  draft_job_post(...)  →  create_post / schedule_post

Applications are stored in .data/applications.json (gitignored). Board outages never break a search: each board's error is reported per-board under board_errors.

Image posts

Pass a local JPG/PNG/GIF path to create_post (or schedule_post): the file is uploaded through LinkedIn's Images API (/rest/images?action=initializeUpload → signed PUT → referenced by urn:li:image: URN in the post) and published with optional alt_text.

Post scheduling

schedule_post stores the post in .data/scheduled_posts.json; while the web server is running it polls every SCHEDULER_POLL_SECONDS (default 30 s) and publishes posts whose time has arrived. publish_at is ISO-8601 — naive values are treated as UTC, or include an offset (e.g. 2026-09-05T15:30:00+05:30). Failures (e.g. token expired) are recorded on the entry as status="failed" with the error, so list_scheduled_posts shows exactly what happened.

Everything the LLM does goes through a provider-agnostic layer — you switch vendors by changing one env var, no code changes:

LLM_PROVIDER=grok     # or: openai | ollama | unorouter

Prerequisites

Step 1 — Create the LinkedIn Developer App

  1. Go to https://www.linkedin.com/developersCreate app.

  2. Fill in the basics (name, LinkedIn Page, logo) and create it.

  3. In Products, request:

    • Sign In with LinkedIn using OpenID Connect — grants openid, profile, email

    • Share on LinkedIn — grants w_member_social, required to post

  4. In Auth, note your Client ID and Client Secret, and add the Authorized redirect URL:

    (For production you'll use your deployed HTTPS URL instead — Phase 2.)

Step 2 — Configure

python -m venv .venv
source .venv/Scripts/activate        # Git Bash on Windows; on macOS/Linux: .venv/bin/activate
pip install -e ".[dev]"

cp .env.example .env                 # then fill in the values

.env essentials:

LINKEDIN_CLIENT_ID=your-client-id
LINKEDIN_CLIENT_SECRET=your-client-secret
LLM_PROVIDER=grok
GROK_API_KEY=your-xai-key

About xAI billing: xAI currently does not accept Indian payment cards for its API. If that's a blocker, just set LLM_PROVIDER=openai or LLM_PROVIDER=ollama — nothing else changes.

Step 3 — Run

Web mode (personal dashboard UI + OAuth + MCP over HTTP):

python -m linkedin_mcp web
# or: uvicorn linkedin_mcp.app:app --reload

Open http://localhost:8000/ for the personal dashboard — no MCP client needed:

  • Compose & publish posts with an image picker, or schedule them for later

  • Upload documents (PDF/DOCX/TXT/MD, 10 MB max) — mark your resume as profile and every job-matching tool uses your real skills instead of bare profile metadata

  • Job search across the four boards + one-click LLM match against your profile

  • Application tracker with status buttons (saved → applied → interview → …)

  • Live scheduled-posts list with publish-now / cancel

The MCP endpoint is still available for clients at:

http://localhost:8000/mcp/

Stdio mode (for Claude Desktop / local MCP clients):

python -m linkedin_mcp

Step 4 — Connect an MCP client

Claude Desktop / Claude Code claude mcp add example (streamable HTTP):

claude mcp add linkedin --transport http http://localhost:8000/mcp/

Or, from any JSON-RPC client, call auth_status first — it tells you exactly what's missing (credentials, login, tokens) instead of failing blindly.

OAuth & token handling

  • Authorization-code flow with a per-login state (validated on callback; 10 min TTL).

  • Access + refresh tokens are persisted to LINKEDIN_TOKEN_FILE (default .data/tokens.json).

  • Expiry/refresh handling: every API call goes through TokenManager, which transparently refreshes the access token when it's within 5 minutes of expiry. If no refresh token is available (LinkedIn grants those to approved apps only), tools raise a clear "re-authorize" error.

  • The current LinkedIn API version header (Linkedin-Version) is configurable via LINKEDIN_API_VERSION — bump it when LinkedIn sunsets the version you use (check https://learn.microsoft.com/en-us/linkedin/marketing/).

LLM provider swap

LLM_PROVIDER

Requires

Notes

grok

GROK_API_KEY

xAI, OpenAI-compatible (https://api.x.ai/v1)

openai

OPENAI_API_KEY

https://api.openai.com/v1

ollama

local Ollama running

http://localhost:11434/v1, no key

unorouter

UNOROUTER_API_KEY

any other OpenAI-compatible endpoint (https://api.unorouter.com/v1)

All three share one implementation (OpenAICompatProvider) because they all speak OpenAI's chat-completions protocol; the factory in llm/factory.py validates the key and produces a provider with a helpful error if you haven't configured one.

Project layout

src/linkedin_mcp/
├── config.py            # pydantic-settings, all env vars, provider selection
├── errors.py            # ConfigurationError, NeedsReauthError, LinkedInAPIError, LLMError
├── services.py          # shared wiring (store, oauth, token manager, client, scheduler)
├── scheduling.py        # scheduled-post store + background publisher loop
├── jobs.py              # keyless job-board search (Remotive/Arbeitnow/RemoteOK/Jobicy)
├── applications.py      # local application tracker store
├── documents.py         # uploads (pdf/docx/txt/md) + resume/profile summary
├── api.py               # REST endpoints backing the dashboard UI
├── server.py            # FastMCP server: 23 tools + resources
├── app.py               # FastAPI app: /auth/* routes + mounted /mcp endpoint
├── __main__.py          # CLI: `python -m linkedin_mcp [web]`
├── llm/                 # swappable LLM layer (base protocol, factory, provider)
└── linkedin/            # OAuth flow, token storage/refresh, REST client
tests/                   # pytest (mocked HTTP), 66 tests

Security notes

  • Never commit .env or .data/ — both are gitignored.

  • Tokens are stored in plaintext on disk (local dev). For a deployed version, move them to an encrypted store / server-side secret manager.

  • The OAuth state registry is in-memory — fine for a single-user local app; replace with a signed cookie or session store when deploying.

  • The LLM layer only ever sends what you pass to draft_post/analyze_post to the provider — the server never silently uploads your LinkedIn data.

Tests

pytest                 # 59 tests: tokens/refresh, LLM factory, LinkedIn client,
                       # job boards, application tracker, tools (all mocked HTTP)
ruff check src tests   # lint

Roadmap

  • Phase 2 — free hosting: containerize, deploy on a free tier (limits change often — verify current offers), HTTPS, point LINKEDIN_REDIRECT_URI at the deployed URL.

  • Phase 3 — more tools: get_post_statistics (requires approved analytics permissions), video/document posts, an agent loop where the LLM picks tools itself, Adzuna board (free key, aggregates thousands of sites incl. India locations), resume storage + per-JD tailoring.

  • Hardening: signed-cookie state, token encryption, rate-limit handling with retries.

License

MIT

Latest Blog Posts

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/iThinkAndCode/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server