linkedin-safe-mcp
An MCP server that gives AI agents (Claude Code,Codex, Claude Desktop, Cursor, …) LinkedIn superpowers — without putting yourLinkedIn account at risk:
- Post to LinkedIn — text, links, and images via LinkedIn's official API(OAuth, ToS-compliant), plus comments and likes.
- Search jobs — keyword/location/remote/experience/date filters via LinkedIn'spublic guest endpoints. No login, no cookies: your account is never involved.
- Run a job hunt — a local SQLite application tracker (interested → applied →interviewing → offer) with notes and per-job posting snapshots, so an agent canmanage your pipeline and write tailored cover letters even after a posting istaken down.
Why this design?
LinkedIn offers no official job-search API, and the unofficial routes (Voyagerinternal API with your li_at session cookie, headless browsers on your logged-insession) violate LinkedIn's User Agreement §8.2 and routinely get accountsrestricted. This server deliberately splits the difference:
| Concern | How it's handled | Account risk |
|---|---|---|
| Posting, comments, likes | Official REST API, your own OAuth app, w_member_social |
None — sanctioned |
| Job search & details | Guest endpoints (the logged-out jobs pages), IP-rate-limited | None — no credentials involved |
| Pipeline tracking | Local SQLite on your machine | None — never touches LinkedIn |
| Easy Apply, DMs, feed reading | Intentionally not included — impossible without ToS-violating access | — |
Requirements
- Python 3.11+ and uv
- For posting only: a free self-serve LinkedIn developer app (5-minute setup below).Job search and the tracker work with zero setup.
Install & connect to your agent
Clone/copy this directory, then register it with your MCP client. <REPO> below isthe absolute path to this project.
Claude Code
claude mcp add linkedin \
--env LINKEDIN_CLIENT_ID=your_client_id \
--env LINKEDIN_CLIENT_SECRET=your_client_secret \
-- uv run --directory <REPO> linkedin-safe-mcp
Or in a project's .mcp.json:
{
"mcpServers": {
"linkedin": {
"command": "uv",
"args": ["run", "--directory", "<REPO>", "linkedin-safe-mcp"],
"env": {
"LINKEDIN_CLIENT_ID": "your_client_id",
"LINKEDIN_CLIENT_SECRET": "your_client_secret"
}
}
}
}
Codex (~/.codex/config.toml)
[mcp_servers.linkedin]
command = "uv"
args = ["run", "--directory", "<REPO>", "linkedin-safe-mcp"]
env = { LINKEDIN_CLIENT_ID = "your_client_id", LINKEDIN_CLIENT_SECRET = "your_client_secret" }
Claude Desktop (claude_desktop_config.json) — same JSON shape as .mcp.jsonabove.
The LINKEDIN_CLIENT_* variables are only needed for posting; omit them if youonly want job search + tracking.
Enabling posting (one-time LinkedIn app setup)
- Go to https://www.linkedin.com/developers/apps → Create app (requiresassociating any LinkedIn Page; you can create a trivial one).
- On the app's Products tab, add Share on LinkedIn and Sign In withLinkedIn using OpenID Connect.
- On the Auth tab, add the redirect URL
http://localhost:8765/callback. - Copy the Client ID and Client Secret into the env vars shown above.
- Authenticate once — either way works:
- In a terminal:
uv run --directory <REPO> linkedin-safe-mcp auth - Or just ask your agent to post something; it will call the
logintool andhand you the authorization URL.
- In a terminal:
Tokens are stored in ~/.linkedin-mcp/tokens.json (mode 0600) and last ~60 days;LinkedIn doesn't issue refresh tokens to self-serve apps, so you re-run the loginwhen it expires (auth_status tells the agent exactly when that is).
Tools
| Tool | Needs auth | What it does |
|---|---|---|
auth_status |
– | Reports config/auth state with exact next steps |
login / logout |
– | Browser OAuth flow / delete stored tokens |
get_my_profile |
✓ | Name, email, person URN of the connected account |
create_post |
✓ | Publish a post: text (+hashtags), optional link or local image; PUBLIC or CONNECTIONS |
delete_post |
✓ | Delete one of your posts (URN or post URL) |
comment_on_post |
✓ | Comment on a post (URN or post URL) |
like_post |
✓ | Like a post (URN or post URL) |
search_jobs |
– | Filters: location, remote/hybrid/onsite, time posted, experience levels, job types, Easy-Apply-only, sort; up to 50 results |
get_job |
– | Full posting: description, seniority, type, salary if listed, applicant count, external apply URL |
save_job |
– | Snapshot a job into the local tracker |
get_saved_job / list_saved_jobs |
– | One job with history / pipeline overview with status counts |
update_job_status |
– | interested → applied → interviewing → offer / rejected / withdrawn / archived, with notes |
add_job_note / remove_saved_job |
– | Append a note / drop a job |
Things agents can do with this: "find remote staff-engineer roles posted this week,save the promising ones, draft tailored cover letters from the saved descriptions,mark the ones I applied to, and post a summary of my open-source work."
Configuration
| Env var | Default | Purpose |
|---|---|---|
LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET |
– | LinkedIn app credentials (posting only) |
LINKEDIN_MCP_DIR |
~/.linkedin-mcp |
Where tokens + tracker DB live |
LINKEDIN_REDIRECT_PORT |
8765 |
OAuth callback port (must match the app's redirect URL) |
LINKEDIN_API_VERSION |
202606 |
LinkedIn-Version header for /rest/* calls |
LINKEDIN_POSTS_BACKEND |
auto |
rest, ugc, or auto (try + remember what your app is allowed to use) |
LINKEDIN_MCP_USER_AGENT |
a Chrome UA | UA for guest job requests |
Behavior notes & limits
- Posting: LinkedIn caps member posting at 150 requests/day and rejectsexact duplicates of recent posts (422). Reserved characters in post text areescaped automatically for the versioned API so parentheses don't cause errors;hashtags are preserved.
- Job search: guest endpoints are rate-limited per IP (HTTP 429). Theserver caches results (10 min searches / 6 h job details), retries with backoff,and paces multi-page fetches; on a persistent 429 it returns a clear "wait aminute" error to the agent. Keep
limitmodest. - Scraping posture: guest job search reads the same public pages a logged-outvisitor sees, at human-ish rates, with caching to minimize load. Still, LinkedIncould change or gate these endpoints at any time — the parsers are pinned byfixture tests so breakage is detected loudly, and the tool errors stayagent-actionable.
Development
uv sync # install deps (Python ≥3.11)
uv run pytest # 48 tests: parsers vs live fixtures, payloads, OAuth, tracker,
# plus an end-to-end stdio smoke test that spawns the real server
uv run ruff check src tests && uv run ruff format --check src tests
Layout: src/linkedin_mcp/ — server.py (tool surface) · api/ (official REST:posts, social actions, uploads, dual rest/ugc backend) · auth/ (OAuth + tokenstore) · jobs/ (guest client, HTML parsers, filter mappings) · tracker/(SQLite store) · cli.py (serve | auth | status | logout).
Roadmap
- Publish to PyPI (
uvx linkedin-safe-mcpone-liner) - Reaction types beyond like; multi-image posts; poll posts
- Optional third-party job-data providers behind the same tool schema
streamable-httptransport for remote/hosted use- (Considered, opt-in only, off by default) a cookie-based Voyager provider forpersonalized features — with loud warnings, since it violates LinkedIn's ToS
License
MIT