linkedin-agent-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@linkedin-agent-mcp search for remote software engineer jobs"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
linkedin-agent-mcp
Experimental project. Use entirely at your own risk.
This is a personal engineering experiment in agent safety design (typed tools, out-of-band human confirmation, failure-honest errors). It is published to show the code, not as a product or a recommendation to automate LinkedIn.
How I have used it: only on my own account, a handful of times, with me watching: read-only checks (search, a job page, my own profile, my saved jobs) and a single save/unsave of one posting to verify the confirmation flow. I have never used it to scrape data, send messages or connection requests, apply to jobs, or run anything in bulk or unattended.
LinkedIn's terms: LinkedIn's User Agreement (§8.2) prohibits bots and other automated access, and this project drives a real browser on a real account. Running it may get your account restricted or banned.
Your responsibility: anyone who runs this code does so at their own risk and is solely responsible for complying with LinkedIn's terms and applicable law. The software is provided "as is", without warranty of any kind (see LICENSE), and the author accepts no liability for how it is used.
A personal, human-in-the-loop LinkedIn job-search agent, exposed as an MCP server so an MCP client such as Claude Code can search jobs, read postings and your profile, track what you have seen, and prepare actions, while every consequential step on LinkedIn requires your explicit confirmation, given out-of-band in your own terminal.
It is deliberately not a mass-automation or scraping system: one browser page at a time, small capped result sets, spaced navigation, an hourly cap on external actions, and no evasion techniques of any kind.
At a glance
What it is: a TypeScript (Node 24) MCP server with 20 typed tools that let an AI agent search jobs, read postings and your profile, track what you have seen, and prepare actions on your own LinkedIn account.
Ports and adapters: the MCP layer is thin (zod schemas + error mapping); services depend on a
LinkedInClientinterface and repository interfaces, so Playwright lives in exactly two files and SQLite sits behind swappable repositories.READ / PREPARE / EXECUTE, enforced structurally: each tool group is handed only the ports it may use, so a READ or PREPARE tool cannot reach the executor, not by prompt instructions but by construction.
Out-of-band confirmation: there is no
confirmtool. Every external action needs a human to runnpm run confirmin a real terminal and type a code; the payload is SHA-256 bound to that approval, single-use (database compare-and-swap), and time-limited.Failure-honest: ambiguous writes become
OUTCOME_UNKNOWNand are never auto-retried; LinkedIn markup changes surface as a typedLINKEDIN_CHANGED, never as made-up data; the audit log is append-only via database triggers.Tested: 199 vitest tests (parsers on captured/modelled fixtures, state machines, the whole confirmation workflow including tampering and races, and a spawned stdio server through the real MCP SDK client), plus an opt-in read-only E2E suite run against a real account.
Documented decisions: 13 ADRs in docs/decisions/.
Status (2026-09-23): read-only flows (auth, search, job detail, own profile, saved jobs) are verified live on a real signed-in account, and so is the one registered write, Save/Unsave, run once end to end through the terminal confirmation with the owner present. Easy Apply, messaging, connections and profile writes are not implemented yet. Details in Verification status and CLAUDE.md.
Read this first: ToS and status. LinkedIn's User Agreement (§8.2) prohibits bots and other automated access. There is no official LinkedIn API for job search, saved jobs or applications for job seekers (see LinkedIn API limitations). This project drives a real browser on your own account. That is a risk to your account that only you can accept, and the browser will not start until you have (
npm run login). Also see Verification status: parts are verified live, parts are only unit-tested.
Contents
Architecture · Why MCP · Stack · READ / PREPARE / EXECUTE · Security model · Install · Authentication · Configuration · Claude Code · Tools · Confirmation · Application workflow · Persistence · Testing · Verification status · Limitations · Roadmap · Responsible use · Decision log
Related MCP server: linkedin-mcp-server
Architecture
flowchart TB
CC["Claude Code / MCP client"] -->|"stdio JSON-RPC"| MCP["MCP server (thin: schemas, kind labels, error mapping)"]
subgraph tools["Tool groups get only the ports they may use"]
R["READ tools"]
P["PREPARE tools"]
E["EXECUTE tools (actionId only)"]
end
MCP --> R & P & E
R --> JS["JobService / ProfileService"]
P --> JS
P --> AP["ActionPreparer"]
E --> AX["ActionExecutor"]
AX --> AS["ActionService<br/>state machine, hash check, CAS, audit"]
AP --> AS
JS --> LC["LinkedInClient (interface)"]
AS -->|"executors"| LC
LC --> BC["Browser client (Playwright)"]
LC -.->|"future: official API where one exists"| API["LinkedIn API"]
BC --> BM["BrowserManager<br/>persistent profile, 1 page, throttled, linkedin.com only"]
BM --> LI[("linkedin.com")]
JS & AS & AP --> REPO["Repository interfaces"] --> DB[("SQLite<br/>jobs · job_views · applications · application_answers<br/>profile_changes · messages · actions · audit_events")]
HUMAN(["You, in a terminal"]) -->|"npm run confirm (TTY + typed code)"| CF["ConfirmationService"] --> ASRules the layout enforces:
The MCP layer is thin. Tools validate input (zod), call a service, and map errors. No business logic, no SQL, no Playwright.
Services depend on interfaces.
LinkedInClienthides how LinkedIn is reached; repositories hide where state lives.Only two files import Playwright:
src/browser/browser-manager.ts(site-agnostic) andsrc/linkedin/browser-client.ts(LinkedIn-specific). All DOM knowledge is insrc/linkedin/selectors.ts; parsing is pure functions insrc/linkedin/parsers/.The confirmation port is not reachable from MCP. See Confirmation workflow.
src/
index.ts server.ts container.ts config.ts logger.ts consent.ts state-machine.ts
browser/ browser-manager.ts persistent context, serialization, throttling, allowlist
linkedin/ linkedin-client.ts (interface) browser-client.ts session.ts url.ts selectors.ts types.ts
parsers/ (search, job, profile, dom)
jobs/ profile/ messaging/ applications/ actions/ services + state machines
persistence/ database.ts migrations.ts repositories/
tools/ define.ts schemas.ts views.ts read/ prepare/ execute/
cli/ login.ts confirm.ts
errors/ errors.ts
docs/decisions/ 13 ADRs + index + template scripts/decisions.mjs
tests/ fixtures/Why MCP
MCP gives an agent typed, discoverable tools over a standard protocol, so the same server works with Claude Code today and other clients later. It also gives a natural place to put the safety model: the tool surface (what exists, what each accepts) is part of the security boundary. There is no confirm tool, and execute tools accept only an actionId.
Why this technology stack
Layer | Choice | Why (details in the ADR) |
Language/runtime | TypeScript on Node 24, run directly via native type stripping | MCP SDK and Playwright are TypeScript-first; no loader dependency (ADR-0001) |
MCP |
| Current stable line; local transport; no network surface (ADR-0003) |
Validation | zod 4 (input and output schemas) | Types vanish at runtime; SDK validates both directions |
Browser | Playwright, persistent profile, system Chrome/Edge | Manual login, no credentials handled (ADR-0006) |
Parsing | cheerio + pure functions | Fixture-testable without a browser (ADR-0008) |
Persistence |
| No native addon; replaceable by Postgres (ADR-0005) |
Tests | vitest + MCP SDK client (in-memory and real stdio) | (ADR-0011) |
Four runtime dependencies in total. Logging and state machines are hand-rolled on purpose.
READ / PREPARE / EXECUTE
Kind | May do | May not do | Enforced by |
READ | Read LinkedIn; write local tracking (jobs seen, views) | Change LinkedIn; create actions | READ tools are handed only read ports ( |
PREPARE | Write local state: drafts, | Perform any external action | PREPARE tools are handed |
EXECUTE | Change LinkedIn, for one confirmed action | Accept a payload from the agent; run unconfirmed/expired/altered/repeated actions |
|
This is enforced structurally (interfaces handed to each group, state machine, database), not by prompt instructions. The prompt-level guidance in the server instructions is only a courtesy on top.
Security model
Threat model: an LLM (possibly manipulated by hostile text in a job posting) acting only through MCP tools and non-interactive shell, on a machine you control. Out of scope: malware or a person with full access to your account/machine (they could edit the SQLite file or read your browser profile).
Concern | Control |
Agent performs an external action unprompted | Execute tools need a |
Agent changes what gets executed after approval | Payload is immutable; its SHA-256 is bound to the confirmation and re-verified at execution (tested by tampering with the DB row) |
Replay / double execution | Confirmation is single-use: |
Stale approval | Confirmation expires (default 10 min); pending actions expire (60 min) |
Ambiguous write outcome (browser dies mid-submit) |
|
Runaway agent | Hourly cap on executed actions; 25-result cap; single serialized page; spaced navigation |
Credential exposure | Passwords, MFA and cookies are never handled by this program: login is manual in a browser window; logs redact sensitive keys; no tool returns them; cookie values are never read (only presence of |
Browser used elsewhere | Host allowlist: only |
Prompt-injected data | Tool inputs are strictly validated; free text is only ever stored/previewed, never executed; LinkedIn URLs are parsed and canonicalised |
Error leakage | Unknown errors collapse to |
Tampering with history |
|
Local secrets | Browser profile, DB and consent live under |
Defence in depth: .claude/settings.json adds Claude Code ask rules for the execute tools.
Installation
Requirements: Node.js ≥ 24, and Google Chrome or Microsoft Edge installed (or npx playwright install chromium and BROWSER_CHANNEL=chromium).
git clone <this repo> linkedin-agent-mcp && cd linkedin-agent-mcp
npm install
npm run build
npm testAuthentication
npm run loginFirst run only: shows the ToS/automation notice; you must type
I UNDERSTAND. (Refuses to run without an interactive terminal.)Opens a visible Chrome/Edge window on a dedicated profile (
~/.linkedin-agent-mcp/browser-profile).You sign in: password, MFA, and any CAPTCHA (the program never automates or bypasses these).
It detects the session (presence of the
li_atcookie and a non-wall page), double-checks by loading the feed, and closes the browser so the profile is saved.
Re-run when the session expires (LOGIN_REQUIRED). Check state any time with the linkedin_auth_status tool.
Configuration
Typed and validated at startup (src/config.ts); see .env.example. No LinkedIn credentials exist in configuration.
Variable | Default | Meaning |
|
| Root for all local state |
|
| Browser profile |
|
| SQLite file |
|
| ( |
|
|
|
|
|
|
|
| Hard cap is 25 |
|
| Spacing between page loads |
|
| Confirmation validity |
|
| Cap on executed external actions |
|
| Save HTML on |
npm run confirmreads the same variables. If you overrideLINKEDIN_AGENT_HOME/DATABASE_PATHfor the MCP server, set them in your shell too, or the CLI will look at a different database.
Claude Code integration
Syntax verified against the current Claude Code docs (claude mcp add [options] <name> -- <command> [args...]; options go before the name; -- is mandatory).
Option A: command line (use absolute paths; build first with npm run build):
# Windows example
claude mcp add --scope user linkedin-agent -- node C:\Users\you\code\linkedin-agent-mcp\dist\index.js
# macOS/Linux
claude mcp add --scope user linkedin-agent -- node /home/you/linkedin-agent-mcp/dist/index.js--scope local (default) = this project only, private; --scope project = writes .mcp.json; --scope user = all your projects. node is invoked directly, so the cmd /c wrapper (needed for npx on native Windows) is not required.
Option B: .mcp.json: a ready-made one is in this repo (works when Claude Code is opened in this folder; the first time it asks you to approve the project server):
{
"mcpServers": {
"linkedin-agent": {
"command": "node",
"args": ["dist/index.js"]
}
}
}The path is relative on purpose: stdio servers start in the project folder, and ${CLAUDE_PROJECT_DIR} is not expanded in .mcp.json (Node received the literal text and exited, which showed up as CONNECTION_CLOSED; the real error is in %LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-linkedin-agent). Run npm run build first, and restart the session (or /mcp → Reconnect) after changing this file.
For use from other projects, put an absolute path in args, or use Option A with --scope user. Verify with claude mcp list, or /mcp inside a session. To debug interactively: npm run inspect (MCP Inspector).
Recommended permissions (.claude/settings.json is included). Keep the execute tools on ask, never allow:
{ "permissions": { "ask": ["mcp__linkedin-agent__linkedin_save_job", "mcp__linkedin-agent__linkedin_unsave_job"] } }Available tools
Every description is prefixed with its kind. Full schemas are visible via tools/list.
READ
Tool | Purpose |
| Is the browser profile signed in? (one page load) |
|
|
| Full posting by URL; unknown fields are omitted, never guessed |
| LinkedIn's own Saved-jobs list (My items → Job tracker → Saved), |
| Your own profile; |
| Company "About" page |
| Local tracking only (no LinkedIn access) |
| State + exact preview of prepared actions |
| Local state of an in-progress application: status, CV, fields, every question with its answer/source/ |
| Exact human-readable payload (job / CV / fields / questions) to review before any confirmation; |
PREPARE (local state only)
Tool | Purpose |
| Local status: |
|
|
| Current vs proposed headline/about (reads current value from LinkedIn) |
| Stores a draft; nothing is sent |
| Stores a request preview; nothing is sent |
| Records the user's own answer to one application question that could not be established automatically; never fabricated |
| Cancels a pending/confirmed action |
EXECUTE (requires a user-confirmed action)
Tool | Purpose |
| Take only |
Planned, not registered so the agent never meets a dead end: submit_application, update_profile, send_message, send_connection_request, prepare_application. See ADR-0009.
Example
// 1. agent → linkedin_search_jobs
{ "keywords": "NestJS", "location": "Tel Aviv, Israel", "workType": "hybrid", "maxResults": 5, "onlyNew": true }
// ← { "jobs": [{ "id": "3812345678", "title": "...", "company": "...", "isNew": true, "localStatus": "discovered", ... }],
// "fetched": 5, "newCount": 3, "filteredOut": 2 }
// 2. agent → linkedin_prepare_save_job { "url": "https://www.linkedin.com/jobs/view/3812345678/" }
// ← { "action": { "actionId": "26751bd3-…", "status": "PENDING", "preview": "SAVE \"…\" at … on LinkedIn" },
// "executionAvailable": true, "nextStep": "Show the preview… run `npm run confirm -- 26751bd3`…" }
// 3. YOU, in a terminal: npm run confirm -- 26751bd3 (shows the preview, asks you to type a code)
// 4. agent → linkedin_save_job { "actionId": "26751bd3-…" } ← COMPLETED (single use)Confirmation workflow
prepare tool ─► PENDING ──(you: npm run confirm, TTY + code)──► CONFIRMED ──► EXECUTING ──► COMPLETED
│ │ ▲ │ │ ├─► FAILED (proven: nothing happened)
│ │ └── payload hash fixed here │ │ └─► OUTCOME_UNKNOWN (ambiguous: never auto-retry)
│ └─► CANCELLED / EXPIRED └─► CANCELLED / EXPIRED (10 min)
└─ returns actionId + exact previewWhy out-of-band? The agent calls the tools, so any in-band
confirmed:truecan be forged. See ADR-0004.npm run confirm(list) ·npm run confirm -- <id>(review + confirm) ·-- --cancel <id>·-- --audit <id>·-- --resolve <id> completed|failed(after anOUTCOME_UNKNOWN, once you have checked LinkedIn)."Apply to good jobs", "go ahead with all of them", or any vague instruction is not a confirmation: each action needs its own confirmation of its exact payload.
Application workflow
Target flow: search → inspect → analyze → prepare_application → (needs_user_input | ready_to_submit) → your review → your confirmation → submit_application.
Implemented today: the guarded core: ApplicationService and the application state machine (DISCOVERED → PREPARING → NEEDS_USER_INPUT ⇄ READY_TO_SUBMIT → CONFIRMED → SUBMITTING → SUBMITTED, plus FAILED, SUBMISSION_UNCERTAIN, EXTERNAL, ABANDONED), with these rules enforced and tested: answers are never fabricated (unknown ⇒ needs_input); READY_TO_SUBMIT is impossible with open questions; external applications are detected and reported, never auto-submitted; SUBMITTED is terminal; an uncertain submission can only be resolved, never retried; the preview format (Job / Company / Position / CV / fields / questions / final action) is built by buildPreview. The user-input-collection + preview half of the tool surface is registered (linkedin_get_application, linkedin_provide_application_answer, linkedin_preview_application, 2026-09-22) — but since nothing yet creates an application with real questions (that is prepare_application, next), these tools have no reachable data on the real account until then; they are unit- and protocol-tested against directly-seeded application state.
Not implemented: the Easy Apply form driver, prepare_application and submit_application. A real signed-in session now exists, but no Easy Apply form has been captured yet; the driver is built from a real, read-only capture of one (see Roadmap), starting with prepare_application, never with submit. They are not exposed as tools.
Persistence
SQLite at ~/.linkedin-agent-mcp/agent.db (WAL). Tables: jobs (unique canonical_key = li:<jobId> for dedupe, first_seen_at, last_seen_at, seen_count, status), job_views, applications, application_answers, profile_changes, messages, actions, audit_events (append-only). Job statuses: discovered, viewed, considering, saved, rejected, preparing, ready_to_submit, submitted, failed, changed only through a transition table. Migrations are forward-only (src/persistence/migrations.ts).
Testing
npm test # unit + protocol tests, no network
npm run typecheck
LINKEDIN_E2E=true npx vitest run tests/e2e # OPT-IN, real account, read-only, see file headerPowerShell: $env:LINKEDIN_E2E="true"; npx vitest run tests/e2e
Profile lock. The E2E suite and the MCP server share one persistent browser profile, and Chrome allows one process per profile. If the MCP server has used the browser recently (any browser-backed tool call, including
linkedin_auth_status), the E2E run fails immediately with "Could not start the chrome browser" until the server closes its idle browser (BROWSER_IDLE_CLOSE_MS, default 5 min). Run E2E with the MCP server disconnected, or wait.
Covers: input validation through the real MCP SDK client; job parsing (synthetic and real captured fixtures); job-ID extraction and canonicalisation; deduplication (in the database and in the service); application, action and job state machines (including forbidden transitions such as SUBMITTED → PREPARING); the full confirmation workflow (unconfirmed, expired, wrong type, tampered payload, single-use, race, hourly limit, non-TTY, wrong code); failure classification (FAILED vs OUTCOME_UNKNOWN) and audit trail; error mapping and leak checks; logger redaction; the consent gate; repository behaviour incl. append-only triggers; and a real spawned stdio server proving stdout carries only JSON-RPC.
Verification status
Honest accounting of what has and has not been exercised.
Area | Status |
Build, typecheck, unit + protocol + spawned-stdio tests | ✅ Run, passing |
Real Chrome launch with persistent profile; consent gate; session probe | ✅ Verified live (signed out) |
Job search parsing on real LinkedIn markup, with location filter and | ✅ Verified live, signed-out/guest view (React, Java queries) |
Job detail parsing | ✅ Verified against a captured real signed-out page (fixture) |
Dedupe across separate runs; invalid URL; 404 → | ✅ Verified live / with real captured wall |
NestJS search | ⚠️ Signed out: LinkedIn returned HTTP 999 (throttling); handling of the 999 itself is verified. ✅ Signed in: verified live alongside React and Java (2026-09-20) |
Logged-in login, session probe, search, job detail, profile (name, headline, location, About, Experience, Education, Skills incl. the | ✅ Verified live on a real account, 2026-09-20 ( |
Signed-in fixtures | ⚠️ Modelled on the real markup (structure, hashed classes, |
Save/Unsave click (prepare → | ✅ Verified live once, owner present, on a throwaway job, 2026-09-23; the role-based button lookup worked unchanged |
Grouped multi-role experience entries, company page, Easy Apply | ❌ Not verified on a real account (the test profile has no grouped roles) |
Saved-jobs list ( | ✅ Probed live (read-only) 2026-09-22; fixture is modelled, not a capture (real content deleted). |
| ✅ Run by the owner in a real terminal during the Save/Unsave check (2026-09-23). Its logic ( |
Easy Apply, submit, messaging, connection requests, profile writes | ⛔ Not implemented |
Limitations
Selectors will break when LinkedIn changes markup. Failure mode is a typed
LINKEDIN_CHANGED, never fake data;DEBUG_SNAPSHOTS=truesaves the page for a quick fix.Signed-out pages are throttled by LinkedIn within a few requests (HTTP 999); the intended mode is signed in.
The signed-in layout has no
<h1>, no stable class names and no<li>in profile lists. It is read through<title>, headings,data-testidandcomponentkeyanchors (ADR-0013); expect these to churn too.linkedin_get_profileis slow by design (main page, lazy-load scrolling, up to three "show all" pages, spaced navigation): tens of seconds. Make sure the MCP client's tool timeout allows it.The E2E suite and the MCP server cannot use the browser profile at the same time (see Testing).
In signed-out views an "Apply" button is ambiguous, so
easyApply/applyTypeare reported as unknown.One browser page at a time; a search returns at most 25 results by design.
node:sqliteis still flagged experimental by Node.The confirmation model does not protect against something with full shell/database access to your machine.
LinkedIn API limitations
Officially available to any developer: Sign in with LinkedIn (OpenID Connect) and Share on LinkedIn. Job-related APIs (Talent Solutions: Job Posting, Apply Connect, Recruiter System Connect, …) are partner-gated and built for employers/ATS vendors. There is no official API for a job seeker to search jobs, read saved jobs, apply, message, or read their own full profile. LinkedInClient is the seam to swap in an official API if that changes.
Browser automation limitations
Violates LinkedIn ToS §8.2 on its face (see top); subject to bot detection (HTTP 999, CAPTCHA, verification checkpoints: all surfaced as typed errors and never bypassed); DOM churn; sessions expire; needs a real browser installed.
Roadmap
Done: signed-in read-only flows verified live and parsers fixed (2026-09-20, ADR-0013); Chrome-exit-at-launch now classified as a profile lock, not "could not start Chrome" (2026-09-22); get_saved_jobs implemented, unit-tested against a modelled fixture, registered, and verified live by tests/e2e (2026-09-22); user-input-collection + application-preview tools (linkedin_get_application, linkedin_provide_application_answer, linkedin_preview_application) implemented, registered, unit- and protocol-tested (2026-09-22) — built ahead of prepare_application, so not yet reachable on the real account; Save/Unsave verified live end to end with the owner present (2026-09-23).
Next, in order (each gated on live verification, read-only first; the same list with working notes is in CLAUDE.md):
prepare_application: open a real Easy Apply form read-only, detect fields/questions, fill only what the profile/CV/config establishes, stop atneeds_user_input, detect external apply. This is where the agentic logic starts, and the first thing that will actually feed data intolinkedin_get_application/linkedin_provide_application_answer/linkedin_preview_application. Do not start withsubmit_application.submit_applicationwith idempotency checks (inspect LinkedIn's "Applied" state before any retry).Only then
update_profile,send_message,send_connection_request.
Later: richer job tracking (notes, follow-ups); optional MCP elicitation as a second confirmation channel; PostgreSQL repositories if ever multi-user.
Responsible use
This is for your own account and a small number of deliberate actions. Do not use it to mass-apply, mass-message, mass-connect or harvest data. Do not point it at other people's accounts. Do not add stealth, CAPTCHA solving or rate-limit evasion: when LinkedIn says no (999, checkpoint, CAPTCHA), stop. You are responsible for compliance with LinkedIn's terms and applicable law, and for the accuracy of anything submitted in your name. Never let an agent fabricate answers on applications; the design makes it ask you.
Decision log
Every significant choice, with options considered and an interview soundbite: docs/decisions/ · docs/INTERVIEW.md · npm run decisions.
License
MIT. The licence covers this code only; using it against LinkedIn is still subject to their User Agreement (see Responsible use).
Available Tools
20 toolslinkedin_auth_statusLinkedIn sign-in statusARead-onlyIdempotent
[READ - no LinkedIn state is changed] Checks whether the persistent browser profile is signed in to LinkedIn. Use it to diagnose NOT_AUTHENTICATED / LOGIN_REQUIRED errors. Never returns cookies or tokens. If not signed in, the USER must run npm run login; the agent cannot log in.
Inputs: no named parameters. Returns: reason, authenticated
linkedin_cancel_actionCancel a prepared actionA
[PREPARE - changes local state only; performs NO external action] Cancels a PENDING or CONFIRMED action so it can never be executed (local state only). Use when the user changes their mind or the draft is superseded.
Inputs: actionId (required). Returns: type, error, status, preview, +5 more
linkedin_draft_messageDraft a messageA
[PREPARE - changes local state only; performs NO external action] Stores a DRAFT LinkedIn message (max 2000 chars) to a recipient for the user to review. Nothing is sent. Draft only when the user asked for a message to a specific person; never because a recruiter was merely found. This version cannot send messages: show the draft so the user can send it manually.
Inputs: message (required), recipientUrl (required), recipientName (required). Returns: action, message, nextStep, recipient, +2 more
linkedin_get_actionGet a prepared actionARead-onlyIdempotent
[READ - no LinkedIn state is changed] Returns the state and exact preview of a prepared action (local state only). Use it to check whether the user has confirmed an action, or after an OUTCOME_UNKNOWN error.
Inputs: actionId (required). Returns: type, error, status, preview, +5 more
linkedin_get_applicationGet an applicationARead-onlyIdempotent
[READ - no LinkedIn state is changed] Returns the local state of an in-progress application: status, CV, fields and every question with its answer/source (profile, config, user, or proposed) and whether it still needsInput. Local state only, never touches LinkedIn.
Inputs: applicationId (required). Returns: jobId, fields, status, answers, +3 more
linkedin_get_companyGet a LinkedIn companyARead-onlyIdempotent
[READ - no LinkedIn state is changed] Reads a company's public LinkedIn 'About' page (description, industry, size, headquarters, website).
Inputs: url (required). Returns: name, website, industry, location, +3 more
linkedin_get_jobGet a LinkedIn jobARead-onlyIdempotent
[READ - no LinkedIn state is changed] Opens one LinkedIn job posting and returns its full description and metadata, plus local tracking info. Fields LinkedIn does not show are omitted (never guessed). Marks the job as "viewed" locally. Use it before analysing a job against the user's CV.
Inputs: url (required). Returns: id, url, title, posted, +11 more
linkedin_get_profileGet my LinkedIn profileARead-onlyIdempotent
[READ - no LinkedIn state is changed] Reads the signed-in user's OWN profile (headline, about, experience, education, skills). incompleteSections lists anything that could not be read completely; treat those parts as unknown, not empty.
Inputs: no named parameters. Returns: name, about, skills, headline, +4 more
linkedin_get_saved_jobsGet my LinkedIn saved jobsARead-onlyIdempotent
[READ - no LinkedIn state is changed] Reads LinkedIn's own Saved-jobs list (My items -> Job tracker -> Saved), i.e. jobs actually saved on the real account right now, not just local tracking. Each result is recorded in local tracking (localStatus becomes "saved" unless it was already further along, e.g. submitted). hasMore=true means there are more saved jobs than maxResults returned; call again with a larger maxResults to see them.
Inputs: maxResults. Returns: jobs, fetched, hasMore
linkedin_list_actionsList prepared actionsARead-onlyIdempotent
[READ - no LinkedIn state is changed] Lists recent prepared actions and their states (local state only), optionally filtered by status.
Inputs: limit, status. Returns: actions
linkedin_list_tracked_jobsList locally tracked jobsARead-onlyIdempotent
[READ - no LinkedIn state is changed] Lists jobs from LOCAL tracking only (no LinkedIn access), optionally by local status (discovered, viewed, considering, saved, rejected, ...). Use it to avoid re-presenting jobs the user already handled.
Inputs: limit, status. Returns: jobs
linkedin_mark_jobMark a job locallyA
[PREPARE - changes local state only; performs NO external action] Sets the LOCAL tracking status of a job the agent already knows (viewed, considering or rejected). Does not touch LinkedIn. Use "rejected" only when the USER said they are not interested, so it is not shown again.
Inputs: url (required), status (required). Returns: id, title, status, company
linkedin_prepare_connection_requestPrepare a connection requestA
[PREPARE - changes local state only; performs NO external action] Prepares (does NOT send) a LinkedIn connection request with an optional note (max 300 chars). Only for a specific person the user named. Never bulk-prepare. This version cannot send connection requests: show the preview so the user can do it manually.
Inputs: recipientUrl (required), recipientName (required), note. Returns: action, nextStep, executionAvailable
linkedin_prepare_profile_updatePrepare a profile changeA
[PREPARE - changes local state only; performs NO external action] Prepares (does NOT apply) a change to the user's own LinkedIn headline (max 220 chars) or about section (max 2600 chars). Reads the current value from LinkedIn and returns current vs proposed as a PENDING action. Never propose changes the user did not ask for. This version cannot apply profile changes: show the preview so the user can apply it manually.
Inputs: field (required), proposedValue (required). Returns: field, action, nextStep, currentValue, +2 more
linkedin_prepare_save_jobPrepare saving/unsaving a jobA
[PREPARE - changes local state only; performs NO external action] Prepares (does NOT perform) saving or un-saving a job on LinkedIn. Returns a PENDING action with a preview. Recommending a job does not require saving it; only prepare this when the user asked to save it. To execute: the user confirms in their terminal, then call linkedin_save_job / linkedin_unsave_job.
Inputs: url (required), mode. Returns: action, nextStep, executionAvailable
linkedin_preview_applicationPreview an application before submissionARead-onlyIdempotent
[READ - no LinkedIn state is changed] Renders the exact human-readable payload for an application (job, CV, fields, every question and its answer) so the user can review it before any confirmation. readyToSubmit is false while any question is unanswered; use linkedin_provide_application_answer for those first. Local state only, never touches LinkedIn.
Inputs: applicationId (required). Returns: status, preview, unanswered, readyToSubmit
linkedin_provide_application_answerAnswer an application questionA
[PREPARE - changes local state only; performs NO external action] Records the USER's own answer to one application question that could not be established from their profile/CV/config. Local state only, nothing is sent to LinkedIn. Never invent the answer yourself: only call this with an answer the user actually gave. Once every question is answered the application becomes READY_TO_SUBMIT; check with linkedin_preview_application.
Inputs: answer (required), answerId (required), applicationId (required). Returns: jobId, fields, status, answers, +3 more
linkedin_save_jobSave a job on LinkedInA
[EXECUTE - external action; REQUIRES the user to have confirmed this exact action] Saves a job on LinkedIn (changes LinkedIn state; reversible with linkedin_unsave_job). Requires an actionId from linkedin_prepare_save_job that the USER has confirmed in their own terminal. Fails with CONFIRMATION_REQUIRED otherwise; do not retry in a loop, ask the user. Never save jobs just because they look good.
Inputs: actionId (required). Returns: data, action, summary
linkedin_search_jobsSearch LinkedIn jobsARead-onlyIdempotent
[READ - no LinkedIn state is changed] Searches LinkedIn jobs and returns at most 25 results (one page). Every result is recorded in local tracking and annotated with isNew / localStatus, so you can tell jobs the user has never seen from ones already seen, saved, rejected or applied to. Set onlyNew=true to return only never-before-seen jobs. Use LinkedIn job IDs (the id field) to refer to jobs. Do not call repeatedly in a loop.
Inputs: keywords (required), onlyNew, location, workType, +4 more. Returns: jobs, fetched, newCount, filteredOut
linkedin_unsave_jobUnsave a job on LinkedInA
[EXECUTE - external action; REQUIRES the user to have confirmed this exact action] Removes a job from the saved list on LinkedIn (changes LinkedIn state). Requires an actionId from linkedin_prepare_save_job (mode "unsave") that the USER has confirmed in their own terminal.
Inputs: actionId (required). Returns: data, action, summary
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 20 tools
Each tool targets a distinct resource and action, with clear READ/PREPARE/EXECUTE labels and local-vs-LinkedIn boundaries. Related pairs like get_action/list_actions and get_application/preview_application are differentiated by singular-vs-list and structured-state-vs-rendered-preview purposes.
All tools use the same linkedin_ prefix and snake_case convention, mostly following a verb_noun pattern. Minor variations like linkedin_auth_status remain readable and do not break the overall consistency.
At 20 tools, the set is on the heavy side and falls into the borderline 16-25 range. While many tools earn their place through the prepare/execute safety model, some pairs could potentially be consolidated.
Core job search, tracking, job detail, and application preparation are covered. However, execute tools are missing for application submission, message sending, connection request sending, and profile update application, creating notable dead ends for end-to-end workflows.
Maintenance
Related MCP Connectors
LinkedIn actions for AI agents: search, messaging, posts and invites, as hosted MCP tools.
A hosted LinkedIn MCP server for your own account. Paste one address into Claude, ChatGPT, Cursor or n8n to read your inbox, send messages and invitations, and search people. Free to start. Not made by LinkedIn.
LinkedIn jobs, companies and people for AI agents, plus messages and invites via your own account.
LinkedIn MCP for Claude, ChatGPT, Grok, Hermes: triage inbox, comment, find people, live profiles
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA Model Context Protocol (MCP) server that provides tools to interact with LinkedIn's Feeds and Job API. You can do "search for 3 data engineer jobs in . For each job check if it a good match for me by analyzing it against my resume in file resume.md."207-
- AlicenseNot gradedqualityDmaintenanceEnables interaction with LinkedIn tools and services through the Universal MCP framework, allowing operations like posting and profile management via natural language.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables natural language interaction with LinkedIn, including profile retrieval, job searching, messaging, and post engagement.3Apache 2.0
- AlicenseAqualityDmaintenanceEnables searching and scraping of LinkedIn profiles, companies, jobs, and posts using natural language through MCP-compatible AI clients.13MIT