linkedin-mcp
# LinkedIn MCP
Unofficial [Model Context Protocol](https://modelcontextprotocol.io) server for **LinkedIn** — read your profile, network and connections, search people and jobs, and send messages from any MCP client (Claude, etc.).
It runs in two modes:
- **HTTP + OAuth 2.1** (recommended) — the server is an OAuth 2.1 authorization server for MCP clients and bridges to **LinkedIn's real 3‑legged OAuth** consent screen. Each user signs in on `linkedin.com`; the server never sees their password.
- **stdio** — a local server that reads a `LINKEDIN_ACCESS_TOKEN` from the environment. Handy for a single user or quick testing.
This mirrors the auth architecture of [`@aol/ifood-mcp`](https://github.com/AriOliv/ifood-mcp) and [`@aol/uber-mcp`](https://github.com/AriOliv/uber-mcp), swapping their reverse‑engineered logins for LinkedIn's first‑class OAuth.
---
## Tools
| Tool | LinkedIn endpoint | Notes |
|------|-------------------|-------|
| `linkedin_me` | `GET /v2/userinfo` | Works with the default `openid profile email` scopes. |
| `linkedin_network_stats` | `GET /v2/networkSizes/{urn}` | Needs `r_1st_connections_size`. |
| `linkedin_connections` | `GET /v2/connections` | Needs an approved connections product. |
| `linkedin_get_profile` | `GET /v2/people/{id}` | Needs an approved profile product. |
| `linkedin_search_people` | `GET /v2/search/people` | Needs an approved search product. |
| `linkedin_search_jobs` | `GET /v2/jobs/search` | Needs an approved jobs product. |
| `linkedin_send_message` | `POST /v2/messages` | Needs `w_member_social` (messaging). |
> **LinkedIn API access.** Out of the box, a standard LinkedIn app only grants
> the OpenID Connect scopes (`openid profile email`), so `linkedin_me` works
> immediately. The other tools call endpoints that LinkedIn gates behind
> **approved products** (Sign In, Marketing, Talent Solutions, etc.). Until your
> app is approved for those, those tools will return **403** — this is a
> LinkedIn platform restriction, not a bug. Add the granted scopes to
> `LINKEDIN_SCOPES` once approved.
---
## Quick start (HTTP + OAuth)
### 1. Create a LinkedIn app
1. Go to <https://www.linkedin.com/developers/apps> → **Create app**.
2. Under **Auth**, copy the **Client ID** and **Client Secret**.
3. Add your callback under **Authorized redirect URLs**:
`http://localhost:3000/login/callback` (or `PUBLIC_URL` + `/login/callback`).
4. Under **Products**, add **Sign In with LinkedIn using OpenID Connect** (and any others you need).
### 2. Configure
```bash
cp .env.example .env
# then edit .env:
# MCP_JWT_SECRET=$(openssl rand -hex 32)
# LINKEDIN_CLIENT_ID=...
# LINKEDIN_CLIENT_SECRET=...
# PUBLIC_URL=http://localhost:3000
```
### 3. Run
```bash
npm install
npm run build
npm run start:http # or: npm run dev (tsx watch)
```
Point your MCP client at `http://localhost:3000/mcp`. On first connect it runs
the OAuth 2.1 flow, redirects you to LinkedIn to sign in, and then calls tools
on your behalf.
---
## Quick start (stdio)
```bash
cp .env.example .env
# set LINKEDIN_ACCESS_TOKEN=... (e.g. from the LinkedIn developer console token generator)
npm install && npm run build
npm start
```
MCP client config:
```json
{
"mcpServers": {
"linkedin": {
"command": "node",
"args": ["/absolute/path/to/linkedin-mcpserver/build/index.js"],
"env": { "LINKEDIN_ACCESS_TOKEN": "AQV..." }
}
}
}
```
---
## How the OAuth bridge works
```
MCP client ──/authorize──▶ this server ──302──▶ /login ──▶ /login/start
│
302 ▼ linkedin.com/oauth/v2/authorization
user signs in on LinkedIn
│
/login/callback ◀──302 code&state──────────────────────┘
│ exchange code → LinkedIn access + refresh token
│ fetch /v2/userinfo → stable member id (sub)
│ store tokens per‑user, mint an MCP auth code
▼
MCP client ──/token──▶ this server ──▶ HS256 JWT (audience‑bound to /mcp)
MCP client ──/mcp (Bearer JWT)──▶ tools call LinkedIn with the member's token
```
- MCP access tokens are short‑lived HS256 JWTs signed with `MCP_JWT_SECRET`,
audience‑bound to the `/mcp` resource (RFC 8707), with rotating refresh tokens.
- Downstream LinkedIn tokens are refreshed transparently when the app is
approved for refresh tokens; otherwise the user re‑authenticates.
- All state is in memory (single process). Swap the stores in `src/http/store.ts`
for Redis/DB to scale horizontally.
---
## Environment
| Var | Required | Default | Purpose |
|-----|----------|---------|---------|
| `PORT` | – | `3000` | HTTP port. |
| `PUBLIC_URL` | HTTP | `http://localhost:PORT` | URL the client + LinkedIn see. |
| `MCP_JWT_SECRET` | HTTP | – | HS256 key (≥32 chars). `openssl rand -hex 32`. |
| `LINKEDIN_CLIENT_ID` | HTTP | – | LinkedIn app client id. |
| `LINKEDIN_CLIENT_SECRET` | HTTP | – | LinkedIn app client secret. |
| `LINKEDIN_SCOPES` | – | `openid profile email` | OAuth scopes to request. |
| `LINKEDIN_REDIRECT_URI` | – | `PUBLIC_URL/login/callback` | OAuth callback override. |
| `LINKEDIN_ACCESS_TOKEN` | stdio | – | Token for stdio mode. |
| `LINKEDIN_API_BASE` | – | `https://api.linkedin.com` | API host override. |
| `LINKEDIN_VERSION` | – | – | `LinkedIn-Version` header (YYYYMM) for `/rest`. |
---
## Layout
```
src/
index.ts stdio server + tool catalog + executeTool + TokenProvider
http-server.ts remote MCP server (Streamable HTTP + OAuth 2.1)
http/
provider.ts LinkedInOAuthProvider (OAuthServerProvider, HS256 JWTs)
login-router.ts /login → LinkedIn consent → /login/callback
linkedin-auth.ts LinkedIn OAuth client (authorize / token / userinfo)
session-provider.ts per-user token lookup + transparent refresh
store.ts in-memory OAuth + downstream token stores
```
## Scripts
| Script | Action |
|--------|--------|
| `npm run build` | Compile to `build/`. |
| `npm run start:http` | Run the HTTP/OAuth server. |
| `npm start` | Run the stdio server. |
| `npm run dev` | `tsx watch` the HTTP server. |
| `npm run dev:stdio` | `tsx watch` the stdio server. |
| `npm run typecheck` | `tsc --noEmit`. |
## License
MIT. See [LICENSE](./LICENSE).
TDQS
Scored across 7 tools
Each tool has a distinct purpose: self-profile, profile by ID, search people, search jobs, send messages, connections list, and network size. Although 'me' and 'get_profile' both retrieve profiles, one is self and the other is arbitrary, so there is no ambiguity. The two search tools clearly target different resources.
All tools share the 'linkedin_' prefix, which is consistent. However, the pattern after the prefix varies: some use bare nouns (me, connections, network_stats) while others use verb_noun (search_people, search_jobs, send_message, get_profile). This is a minor inconsistency but still predictable and readable.
With 7 tools, the server is well-scoped for a LinkedIn integration. Each tool covers a core functionality area without unnecessary bloat, falling well within the typical 3-15 range for a focused server.
The tool surface covers the most common LinkedIn actions: viewing profiles, searching people and jobs, messaging, and managing connections. Missing capabilities like posting updates or managing connection requests are noticeable but not critical, as the core workflows are covered.