Skip to main content
Glama
README.md
# LinkedIn MCP Server  ·  Claude Code plugin

A **local** [MCP](https://modelcontextprotocol.io) server (TypeScript + Playwright) that lets
Claude manage **your own** LinkedIn account and the Company/showcase **Pages you admin** —
post, schedule, read the feed, and read/reply to comments — by driving a real local Chrome
under your existing logged-in web session.

Ships as a **Claude Code plugin**: the MCP server, six slash commands, and a skill that teaches
Claude the safe-use rules. Install with two commands — see [Install](#install-as-a-claude-code-plugin).

---

## ⚠️ Read this first — Terms of Service & risk

- This automates the **LinkedIn website**. It does **not** use the official LinkedIn API.
- **Automating LinkedIn web browsing violates the [LinkedIn User Agreement](https://www.linkedin.com/legal/user-agreement)** and can get your account **rate-limited, restricted, or banned**.
- Use it for **your own** account, at **low volume**, for actions **you initiate**. Do not bulk-post, scrape, or run it across many accounts.
- Run it **locally only** — your own machine, your own residential IP, a **headed** (visible) browser. **Never** run it in the cloud, in Docker, or on a datacenter IP: that fingerprint is exactly what LinkedIn flags.
- Provided as-is, no warranty. You accept the risk.

---

## Requirements

- **Node.js ≥ 22.5** (uses the built-in `node:sqlite` — no native build step).
- **Google Chrome** installed (Playwright drives your system Chrome via `channel: "chrome"`; no separate browser download).

## Install as a Claude Code plugin

The repo is its own single-plugin marketplace:

```
/plugin marketplace add HDHNezherParking-cum-Y638-Intl-Ltd/linkedin-mcp
/plugin install linkedin@linkedin-mcp
```

Then, in order:

```
/linkedin:setup    # installs npm deps + compiles src/ → dist/ (once, ~30s)
/linkedin:login    # opens Chrome; you sign in by hand
```

`/linkedin:setup` is optional — the server bootstraps itself on its first tool call — but running
it up front keeps that first call from stalling behind an `npm install`.

> If Chrome isn't found, either install Google Chrome, or run `npx playwright install chromium`
> and delete the `channel: 'chrome'` option in `src/browser.ts`.

### Slash commands

| Command | What it does |
|---|---|
| `/linkedin:setup` | Install dependencies and build the bundled server. Idempotent. |
| `/linkedin:login` | Check the session; open a browser to sign in if needed. |
| `/linkedin:post` | Draft a post, show it for approval, publish to your profile or a Page. |
| `/linkedin:schedule` | Queue / list / cancel scheduled posts. |
| `/linkedin:comments` | Read comments on a post and draft replies. |
| `/linkedin:doctor` | Diagnose runtime, build, session, and selector drift. |

The bundled **`linkedin-account` skill** loads automatically when a conversation turns to
LinkedIn: tool ordering, the approve-before-publishing rule, the local-scheduler caveat, volume
limits, and where to repair selectors.

### Or: register the server directly (no plugin)

**Claude Desktop** (`claude_desktop_config.json`) or a project **`.mcp.json`**, after
`npm install && npm run build`:

```json
{
  "mcpServers": {
    "linkedin": {
      "command": "node",
      "args": ["/absolute/path/to/linkedin-mcp/dist/index.js"]
    }
  }
}
```

## First run — log in once

1. Run `/linkedin:login` (or call the **`linkedin_login`** tool). A real Chrome window opens.
2. Log in by hand (2FA, email, and device checkpoints all work — you're driving).
3. Your session is saved to `~/.linkedin-mcp/session/` and reused on later runs.
4. `linkedin_session_status` confirms you're logged in.

Re-run `linkedin_login` whenever the session expires (LinkedIn's `li_at` cookie lasts weeks to
months; there's no fixed TTL).

---

## Tools

| Tool | What it does |
|---|---|
| `linkedin_login` | Open a headed browser and wait for you to log in manually. No password stored. |
| `linkedin_session_status` | Report whether you're logged in; warn if page selectors have drifted. |
| `create_post` | Publish a text post to your personal profile. |
| `schedule_post` | Publish later, via LinkedIn's native scheduler **or** the local queue — you pick. |
| `list_scheduled_posts` | List locally queued posts and their status. |
| `cancel_scheduled_post` | Cancel a still-pending locally queued post. |
| `list_my_posts` | List your recent posts (their refs feed `read_post_comments`). |
| `read_post_comments` | Read comments on a post (URL or activity/share URN). |
| `reply_to_comment` | Reply to a comment. |
| `list_pages` | List Company/showcase Pages surfaced to your account. |
| `create_page_post` | Publish a post **as** a Page you admin. |
| `read_feed` | Light snapshot of your home feed. |

## Scheduling — two modes, you choose

`schedule_post` takes a required `scheduler` argument and has **no default**. Claude asks you
which one before scheduling anything.

| | `scheduler: "native"` | `scheduler: "local"` |
|---|---|---|
| Who holds the post | LinkedIn (composer → clock icon) | This server, in `~/.linkedin-mcp/schedule.sqlite` |
| Machine asleep at the due time | Publishes anyway | Publishes late, on the next poll after restart |
| List / cancel from here | No — do it on linkedin.com | Yes |
| Browser runs | Now, to place the schedule | At the due time |
| Time granularity | LinkedIn's own picker (fixed slots, limited horizon) | Any future instant |

> The local scheduler **fires only while this server process is running.** If your machine is off
> at the scheduled time, the post goes out on the next poll after the server is back. Native
> scheduling is the one to use when the send time has to hold.

Either way `scheduledAt` is an ISO-8601 instant (`2026-07-23T14:30:00Z`); for native scheduling it
is converted to this machine's local wall-clock time, which is what LinkedIn's picker expects.

## Configuration

All optional — set as environment variables, or in a `.env` file. Two are loaded, later wins:
`~/.linkedin-mcp/.env` (use this when installed as a plugin — a plugin's own directory is a cache
that updates overwrite) and `./.env` (repo-local, for development). See
[`.env.example`](./.env.example).

| Env | Default | Meaning |
|---|---|---|
| `LINKEDIN_MCP_HOME` | `~/.linkedin-mcp` | Root for session + scheduler state. |
| `LINKEDIN_SESSION_DIR` | `$LINKEDIN_MCP_HOME/session` | Persistent Chrome profile (your logged-in session). |
| `LINKEDIN_DB_PATH` | `$LINKEDIN_MCP_HOME/schedule.sqlite` | Scheduler database. |
| `LINKEDIN_HEADLESS` | `false` | Run non-login actions headless. Login is always headed. |
| `LINKEDIN_LOGIN_TIMEOUT_SEC` | `300` | How long login waits for you. |
| `LINKEDIN_POLL_MS` | `30000` | Scheduler poll interval. |
| `LINKEDIN_PACING_MS` | `400,1200` | Randomized delay between UI steps (`min,max`). |

## Security & secrets

- **No password is ever read, stored, or logged** — login is interactive in a real browser.
- Your **session cookies** and the **scheduler DB** live in `~/.linkedin-mcp/`, outside the repo
  and outside the plugin cache (so a plugin update never wipes your login, and no project
  directory ever ends up holding your cookies). **Treat `~/.linkedin-mcp/session/` like a
  password** — anyone with it can act as you on LinkedIn.
- `.gitignore` covers `session/`, `data/`, `.env`, `*.sqlite`, `dist/`, `node_modules/`.

## Selector maintenance (expect this)

LinkedIn's markup changes constantly. **Every LinkedIn selector lives in one file:**
[`src/linkedin/selectors.ts`](./src/linkedin/selectors.ts). When a tool errors with
*"update src/linkedin/selectors.ts"*, or `linkedin_session_status` warns that a selector drifted,
fix the locator there — nowhere else. The comment / feed / page selectors are **best-effort** and
may need a live tuning pass against your account the first time you use those tools.

## Known limitations

- The exact URL of a new post isn't always captured (LinkedIn doesn't reliably expose it).
- `read_feed`, `list_pages`, and comment parsing are best-effort and sensitive to DOM changes.
- Replying **as a Page** isn't supported (LinkedIn doesn't reliably expose the identity switcher in reply boxes).
- Text posts only — no media/video upload.
- Not the official API; no analytics, messaging, or connection management.

## Development

```bash
npm run typecheck   # tsc --noEmit
npm run build       # compile to dist/
npm test            # node:test — scheduler + validation + plugin manifests (no browser, no network)
npm run smoke       # boot the server in-memory and list tools (no LinkedIn contact)
npm run dev         # run from source via tsx
```

The scheduler is unit-tested with the browser mocked (the worker takes an injectable `poster`).
`test/plugin.test.ts` guards the packaging: the manifests parse, the paths they reference exist,
and every MCP tool pre-allowed in a command is one the server actually registers.
Live LinkedIn flows are verified manually — never in CI (needs a real session and would violate ToS).

### Plugin layout

| Path | Role |
|---|---|
| `.claude-plugin/plugin.json` | Plugin manifest (name, version, author). |
| `.claude-plugin/marketplace.json` | Makes this repo a single-plugin marketplace. |
| `.mcp.json` | Registers the bundled stdio server via `${CLAUDE_PLUGIN_ROOT}`. |
| `scripts/launch.mjs` | Bootstrap: installs + compiles if missing or stale, then starts the server in-process. |
| `commands/*.md` | The `/linkedin:*` slash commands. |
| `skills/linkedin-account/SKILL.md` | Safe-use rules Claude loads on LinkedIn topics. |

A marketplace install is a plain git clone — no `node_modules`, no `dist/` — which is why
`launch.mjs` builds on first run. It compares the newest mtime under `src/` against `dist/`, so a
plugin update rebuilds automatically. All its output goes to **stderr**; stdout is the MCP channel.

> Opening *this repo* as a Claude Code project will surface the root `.mcp.json` as a project
> server. Decline it — `${CLAUDE_PLUGIN_ROOT}` only expands in plugin scope. Register
> `dist/index.js` by absolute path instead (see above).

## License

MIT — see [LICENSE](./LICENSE).

TDQS

A4.1/5.0

Scored across 12 tools

Disambiguation5/5

Each tool targets a distinct action/resource: authentication, session status, immediate post, scheduled post, queue management, post listing, comment reading/reply, page listing/page post, and feed reading. No two tools overlap in purpose.

Naming Consistency4/5

Most tools follow a clear verb_noun pattern (create_post, schedule_post, list_scheduled_posts, cancel_scheduled_post, list_my_posts, read_post_comments, reply_to_comment, list_pages, create_page_post, read_feed). The exceptions are linkedin_login and linkedin_session_status, which use a prefix style that deviates from the rest.

Tool Count5/5

12 tools cover the core LinkedIn automation domain—auth, posting (immediate and scheduled), scheduling queue management, comments, pages, and feed—without excess. This is a well-scoped count for the server's purpose.

Completeness4/5

The posting lifecycle is well covered (create, schedule, list, cancel), along with comments and page posts. Missing operations like editing or deleting posts are minor gaps; agents can work around them, but they are noteworthy for a complete LinkedIn content management surface.

Maintenance

ActivityStale
ResponsivenessNo issues