Skip to main content
Glama
AriOliv
by AriOliv
README.md
# 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

A3.7/5.0

Scored across 7 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues