Skip to main content
Glama
Sabari2005

LinkedIn MCP Server

by Sabari2005

LinkedIn MCP Server

A production-grade Model Context Protocol server that lets an AI assistant operate LinkedIn through an authenticated browser session — profile management, posting, networking, messaging, job search and automated applications.

101 tools across 15 categories. No password login: authentication reuses cookies from a browser where you are already signed in.


Why this design

A few decisions shape everything else, and they are worth stating up front.

Two transports, chosen per operation. Reads prefer LinkedIn's internal Voyager API — one HTTP request returning structured JSON, no browser. Writes go through real DOM automation, because LinkedIn's write endpoints are the least stable part of that API and a malformed request can silently blank a profile section. Each transport falls back to the other.

Selector fallback chains. LinkedIn ships UI changes constantly and its class names are obfuscated. Every element is described by a ranked list of selectors — stable test hooks first, then ARIA roles, then visible text, then structural guesses. A markup change usually breaks one candidate, not all four, turning a hard breakage into a silent fallback.

Errors are data, not exceptions. Every tool returns the same envelope whether it succeeds or fails, and failures carry a machine-readable code plus a remediation string. An agent can read why something failed and adapt, instead of blindly retrying.

Conservative by default. Rate limits, daily action quotas, a confirmation gate on destructive tools, and a global dry-run switch. LinkedIn restricts accounts that behave inhumanly, and a restriction costs far more than a slow run.

It says what it does not know. Sections LinkedIn withholds are reported as unavailableSections. Generated resumes list their gaps. Inferred values — like whether someone is a recruiter — carry a score and the signals behind it. Nothing is silently invented.


Related MCP server: LinkedIn Sales & Navigator MCP Server

Install

Requires Node.js 20+.

From npm

Nothing to install — point your client at npx -y linkedin-mcp-bridge and it fetches the package on first run. See Connect a client.

To install it globally instead:

npm install -g linkedin-mcp-bridge   # also installs the Chromium browser
linkedin-mcp-bridge                   # stdio server
linkedin-mcp-http                     # streamable HTTP server
linkedin-mcp-setup                    # sign in up front (optional)

You still need a LinkedIn session before the tools do anything — see Authenticate below. linkedin-mcp-setup does it in one step.

From source

git clone https://github.com/Sabari2005/linkedin-mcp-server.git
cd linkedin-mcp-server
npm install            # installs deps and the Chromium browser
cp .env.example .env
npm run setup:profile  # sign in by hand; see Authenticate below
npm run build

Verify:

npm run cookies:check   # is the session valid?
npm run tools:list      # what can it do?
npm run smoke           # exercise all 101 tools against your account (dry run)

Authenticate

This server never asks for your password. You sign in to LinkedIn yourself, in a real browser, and the server reuses that session — so 2FA, CAPTCHAs and device confirmations all work normally.

Nothing to configure (default)

Install, add the server to your client, and ask it something:

> Show my LinkedIn profile

A Chrome window opens at the LinkedIn login page.
Sign in there. The request completes on its own.

The first call that needs authentication creates a browser profile, opens it at LinkedIn, and waits. Sign in quickly and the same call returns your data; take longer and it says "sign in, then ask again" while the window stays open — the next request goes through. You only ever sign in once: the session lives in the profile and survives restarts.

The profile lives outside the repo, one per machine:

OS

Path

Windows

%LOCALAPPDATA%\linkedin-mcp\chrome-profile

macOS

~/Library/Application Support/linkedin-mcp/chrome-profile

Linux

$XDG_DATA_HOME/linkedin-mcp/chrome-profile

Set LINKEDIN_AUTO_LOGIN=false to turn this off and get a plain error instead.

Sign in up front (optional)

To choose where the profile lives, or to authenticate before wiring up a client:

npm run setup:profile        # from source
linkedin-mcp-setup           # installed globally

It prints one line for your .env:

LINKEDIN_BROWSER_PROFILE=/path/it/printed

Either way the profile is dedicated on purpose: Chrome 136+ refuses to be automated against your primary user-data directory, and it fails by hanging for three minutes rather than saying so. A separate profile also means you never have to close your everyday Chrome to let the server run.

npm run login

Best when there is no Chrome to keep around — a headless server, a container. It opens a browser once, captures the session to a file, and reuses it. That file is a credential, and it expires when LinkedIn rotates or you change your password.

Manual export via the Cookie-Editor extension also works — see docs/authentication.md.

Set exactly one strategy. LINKEDIN_LI_AT outranks LINKEDIN_COOKIE_FILE, which outranks LINKEDIN_BROWSER_PROFILE. A leftover cookie-file line silently ignoring your profile is the single most common setup failure. The server warns at startup when it sees more than one.

Check what it resolved to at any time:

npm run cookies:check

Connect a client

Two transports, same 101 tools:

Transport

Command

Use it for

stdio

npx -y linkedin-mcp-bridge

Editor and CLI clients that spawn the server themselves

streamable HTTP

npx -y -p linkedin-mcp-bridge linkedin-mcp-http

Hosted clients that can only reach a URL

Every config below uses npx, which downloads the package on first run and needs no clone, no build and no absolute paths. The -y skips the install prompt, which a client launching the server has no way to answer.

On the very first authenticated call the server opens a Chrome window for you to sign in (see Authenticate). Nothing else is required up front.

Replace "command": "npx", "args": ["-y", "linkedin-mcp-bridge"] with "command": "node", "args": ["/absolute/path/to/dist/index.js"] after npm install && npm run build. Use an absolute path — clients do not launch from your project directory.

Claude Code

claude mcp add linkedin --scope user -- npx -y linkedin-mcp-bridge
claude mcp list   # → linkedin: ✔ Connected

Cursor

.cursor/mcp.json in the project, or ~/.cursor/mcp.json globally:

{
  "mcpServers": {
    "linkedin": {
      "command": "npx",
      "args": ["-y", "linkedin-mcp-bridge"]
    }
  }
}

Windsurf

~/.codeium/windsurf/mcp_config.json — same shape as Cursor:

{
  "mcpServers": {
    "linkedin": {
      "command": "npx",
      "args": ["-y", "linkedin-mcp-bridge"]
    }
  }
}

GitHub Copilot (VS Code)

.vscode/mcp.json in the workspace. Note Copilot's key is servers, not mcpServers:

{
  "servers": {
    "linkedin": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "linkedin-mcp-bridge"]
    }
  }
}

Then open the Copilot Chat Agent mode tool picker to enable the LinkedIn tools.

OpenAI Codex CLI

~/.codex/config.toml:

[mcp_servers.linkedin]
command = "npx"
args = ["-y", "linkedin-mcp-bridge"]

Claude Desktop

claude_desktop_config.json:

{
  "mcpServers": {
    "linkedin": {
      "command": "node",
      "args": ["C:\\absolute\\path\\to\\mcp\\dist\\index.js"]
    }
  }
}
  • Windows %APPDATA%\Claude\claude_desktop_config.json

  • macOS ~/Library/Application Support/Claude/claude_desktop_config.json

Hosted clients — ChatGPT, Claude web, Gemini, Antigravity

These cannot spawn a local process; they connect to a URL. Run the HTTP transport:

export MCP_HTTP_TOKEN="$(openssl rand -hex 32)"   # required for any non-loopback bind
npx -y -p linkedin-mcp-bridge linkedin-mcp-http     # → http://127.0.0.1:3000/mcp

Then add it as a custom connector with the URL and an Authorization: Bearer <token> header.

A hosted client cannot reach your laptop directly, so expose the port with a tunnel (cloudflared tunnel --url http://127.0.0.1:3000, ngrok http 3000, or a reverse proxy) and give the client the public URL.

This endpoint acts as you on LinkedIn. Anyone who can reach it can post, message and connect as you. The server refuses to bind to a non-loopback address unless MCP_HTTP_TOKEN is set — do not remove that check, and always terminate TLS at your tunnel or proxy.

Variable

Default

Meaning

MCP_HTTP_PORT

3000

Port to listen on

MCP_HTTP_HOST

127.0.0.1

Bind address; non-loopback requires a token

MCP_HTTP_TOKEN

(unset)

Bearer token required on every request

MCP_HTTP_PATH

/mcp

Endpoint path (/health is always available)

Any other MCP client

Any stdio MCP client works: run npx -y linkedin-mcp-bridge and speak JSON-RPC over stdin/stdout. Note that stdout carries the protocol — all logging goes to stderr and data/logs/.


First run

Start with dry-run enabled so nothing can touch your real account while you explore:

DRY_RUN=true

Every mutating tool then validates its arguments and reports what it would do without doing it. Turn it off when you are ready.

Try these:

Check my LinkedIn session status Read my LinkedIn profile and suggest improvements Search for remote machine learning jobs posted in the last week


What it can do

Category

Capabilities

Profile

Read complete profile · update headline, about, location · add/edit/delete experience, education, certifications, projects, publications, awards, volunteering, languages · manage skills · upload photo and banner · Open To Work · analytics · completeness analysis

Posts

Text, image, video, document/carousel and poll posts · edit · delete · react · comment · reply · repost · save · local drafts · read feed and activity

Network

Search people · connection requests with notes · accept/ignore/withdraw invitations · follow/unfollow · remove connections · list connections, followers, following

Messaging

Read conversations and full history · search · send and reply · archive, delete, mark read/unread

Jobs

Search with every LinkedIn filter · full job details · extracted skills · hiring team · save/unsave · saved and applied lists

Applications

Easy Apply automation with form introspection · answer memory · bulk apply · application history · withdraw

Companies

Company profiles · search · follow/unfollow · employees · hiring teams · open roles

Recruiters

Find recruiters with confidence scoring · personalised outreach drafting · bulk messaging

Documents

Job-fit analysis · tailored resumes · cover letters — all grounded in real profile data

Search

Every vertical: people, jobs, companies, posts, events, groups, schools

Export

Profiles, jobs, companies, connections, conversations, posts, search results → JSON, CSV, JSONL, Markdown

Notifications

Read, mark read, delete

Settings

Read all categories · change the few toggles that are safe to automate · trigger LinkedIn's official data export

Full catalogue: npm run tools:list, or ask the assistant to call linkedin_list_tools.


Example workflows

Tailor an application end to end

Find remote LLM engineer jobs in Germany posted this week. For the three best matches, analyse how well my profile fits, generate a tailored resume for each, and draft a cover letter addressed to someone on the hiring team.

The assistant chains linkedin_search_jobslinkedin_analyse_job_fitlinkedin_generate_resumelinkedin_get_hiring_teamlinkedin_generate_cover_letter.

Bulk apply, safely

Apply to all Easy Apply data engineering jobs in Berlin using resume.pdf. My phone is +49 30 12345678 and I have 5 years of Python experience.

linkedin_apply_to_jobs_bulk introspects each employer's form, answers from what you supplied plus remembered answers, and stops rather than guessing at any required question it cannot answer confidently — reporting exactly which ones, so you can supply them and retry.

Improve a profile

Read my profile, score it, then rewrite my About section to target AI engineering roles and add the three skills I am missing.

Recruiter outreach

Find recruiters hiring AI engineers in Berlin, draft a personalised message for each based on my background, and show me the drafts before sending anything.

More in examples/.


Safety

The defaults assume you would rather be slow than restricted.

Control

Default

Purpose

DRY_RUN

false

When true, mutating tools validate and report but never execute

REQUIRE_CONFIRMATION

true

Destructive tools need explicit confirm: true

RATE_LIMIT_REQUESTS_PER_MINUTE

30

Global request pacing

RATE_LIMIT_CONNECTIONS_PER_DAY

80

Below LinkedIn's ~100/week invitation ceiling

RATE_LIMIT_MESSAGES_PER_DAY

100

Messaging volume cap

RATE_LIMIT_APPLICATIONS_PER_DAY

50

Application volume cap

HUMANIZE

true

Randomised delays and human-like typing

Daily quotas persist to disk, so restarting the server cannot be used to sidestep them.

Raising these limits materially increases the risk of a temporary account restriction.


Configuration

Every setting lives in .env — see .env.example for the annotated list. The most useful:

TRANSPORT_STRATEGY=auto      # auto | api | browser
BROWSER_HEADLESS=true        # false to watch automation live (great for debugging)
LOG_LEVEL=info               # trace | debug | info | warn | error | silent
DEBUG_ARTIFACTS=true         # screenshot + HTML dump on browser failures
CACHE_TTL_MS=300000          # read cache lifetime

Set exactly one authentication source — see Authenticate.


Troubleshooting

PROFILE_LOCKED Something else is using the Chrome profile — Chrome allows exactly one process per user-data directory. Your session is fine; do not re-export cookies. Usually a second copy of this server, a linkedin-mcp-setup window still open, or a Chrome you launched from that profile. Close the extra one, or restart your MCP client if it has accumulated duplicate servers, then retry.

Older versions reported this as AUTH_EXPIRED, which sent people to redo a sign-in that was never broken. If you see that on an old build, upgrade.

AUTH_EXPIRED / AUTH_MISSING Run npm run cookies:check — it reports which strategy resolved and flags conflicting ones. If the session is dead, re-run npm run setup:profile (or npm run login). See docs/authentication.md — in particular the section on cookies exported from the login page, which look valid but are not.

My browser profile is being ignored LINKEDIN_LI_AT and LINKEDIN_COOKIE_FILE both outrank LINKEDIN_BROWSER_PROFILE. Comment them out. npm run cookies:check names the winner.

SELECTOR_NOT_FOUND / UI_CHANGED LinkedIn changed its markup. Check the screenshot in data/artifacts/, then add a candidate to the relevant list in src/browser/selectors.ts. No other code needs to change.

RATE_LIMITED Back off. Lower RATE_LIMIT_REQUESTS_PER_MINUTE. Sustained throttling can escalate.

QUOTA_EXCEEDED A self-imposed safety limit, not LinkedIn. Wait for the window to roll over, or raise the corresponding RATE_LIMIT_* value if you accept the risk.

Browser will not launch npx playwright install chromium. If using LINKEDIN_BROWSER_PROFILE, close Chrome completely — Chromium cannot share a locked profile.

Browser hangs for ~180s, then times out Chrome 136+ refusing to automate a primary Chrome user-data directory. The silent hang is the symptom — there is no error until the timeout. Run npm run setup:profile to create a dedicated profile and point LINKEDIN_BROWSER_PROFILE at that instead.

Watch it work Set BROWSER_HEADLESS=false and LOG_LEVEL=debug.


Architecture

src/
  core/        config, logger, errors, tool registry, response envelope,
               retry/backoff, rate limiting, TTL cache, shared types
  auth/        cookie parsing and normalisation, session lifecycle
  browser/     Playwright manager, LinkedInPage wrapper, selector registry
  voyager/     internal API client and normalized-JSON graph decoder
  domains/     profile, posts, network, messaging, jobs, applications,
               companies, recruiters, search, notifications, settings, documents
  tools/       MCP tool definitions (schemas + descriptions)
  storage/     local store for applications, drafts, remembered answers
  export/      JSON / CSV / JSONL / Markdown writers
  cli/         login, session check, cookie import, tool listing

The registry (src/core/registry.ts) is the keystone: it wraps every tool with argument validation, rate limiting, quota enforcement, dry-run interception, confirmation gating, timing and error normalisation. That is why individual tool handlers stay short — the repetitive parts happen exactly once.


Development

npm run dev        # watch mode
npm run typecheck  # strict type check
npm run build      # compile to dist/

Adding a tool: write the domain logic in src/domains/, define the tool with defineTool() in src/tools/, and add it to the array in src/tools/index.ts. The registry handles everything else.

Smoke test

npm run smoke exercises every registered tool against your live session. It forces DRY_RUN=true, so reads hit LinkedIn for real while writes are intercepted and validated without being sent. Fixtures (a post, a job, a conversation) are discovered from your own account at runtime — nothing is hardcoded.

npm run smoke                      # all 101 tools
npm run smoke -- --only jobs -v    # one category, with failure detail
npm run smoke -- --skip-writes     # reads only
npm run smoke -- --json            # machine-readable report

A handful of tools are skipped by design: those that print live credentials (auth_export_cookies), tear down the session the suite depends on (system_reset, auth_reload), or have irreversible LinkedIn-side effects even in dry run (request_data_export, mark_notifications_read). Tools whose fixture is missing from your account — messaging tools on an empty inbox, for example — are reported as skipped rather than passed.

Releasing

Releases are tag-driven and fully automated by .github/workflows/publish-mcp.yml. Pushing a v* tag publishes the npm package and then registers it with the MCP Registry.

npm version patch          # bumps package.json
# bump "version" and packages[0].version in server.json to match
npm run validate:registry  # fails loudly if anything disagrees
git commit -am "Release v1.0.4" && git tag v1.0.4
git push origin main --tags

server.json is the registry manifest. npm run validate:registry checks it against the $schema it declares using ajv, then verifies the invariants the schema cannot express:

Invariant

Why it matters

package.json mcpNameserver.json name

How the registry proves you own the npm package. Compared byte for byteio.github.Sabari2005 must keep its capitals.

packages[0].identifierpackage.json name

The registry resolves the npm package from this field.

tag ≡ package.jsonserver.json version

Three places, one number. The workflow refuses to publish a release whose tag disagrees.

repository.url owner ≡ namespace owner

GitHub OIDC only grants io.github.<owner>/*.

The workflow authenticates to the registry with GitHub OIDC, so the only secret the repository needs is NPM_TOKEN (an npm automation token). npm publish runs first — registry validation fetches the published package — and is skipped if that version is already on npm, so a re-run after a partly failed release still completes.


Limitations

Stated plainly, because silent failure is worse than a clear "no":

  • Premium features (InMail to strangers, full analytics, Recruiter) need a paid account. Tools report this rather than failing obscurely.

  • Scheduled posting is not supported — LinkedIn's scheduler has no automatable surface. Use local drafts plus your own scheduler.

  • Most account settings are read-only here. LinkedIn's settings UI has no stable hooks and a mis-click has real privacy consequences, so only unambiguous toggles are writable; the rest come with direct links.

  • Skill extraction from job descriptions is keyword matching against a curated vocabulary — deliberately conservative, since a false "requires Rust" is worse than an omission.

  • Recruiter identification is inferred from headline patterns; LinkedIn has no recruiter entity type. Every match carries a score and its reasoning.

  • Resume generation assembles and organises real profile data. It never invents experience, and reports what it could not source.


Responsible use

This automates your own LinkedIn account. You remain responsible for what it does.

LinkedIn's User Agreement restricts automated access. Accounts that behave inhumanly get restricted. The conservative defaults exist for that reason — keep them, and prefer targeted actions over volume.

Do not use this to spam, scrape people who have not consented, or misrepresent qualifications. Generated resumes and letters are drafts built from your real profile: review them before they reach another person.


License

MIT

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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

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