linkedin-mcp
by iamvaibhav31
README.md
# LinkedIn MCP Server — Remote Edition (Claude + ChatGPT)
An MCP server exposing LinkedIn actions, reachable from **both Claude and ChatGPT**
as a custom connector. Runs over HTTP (not stdio) because ChatGPT only supports
remote, HTTPS-reachable MCP servers — it cannot launch a local script the way
Claude Desktop can.
## What this can and can't do
LinkedIn's public API does not expose any endpoint for a third-party app to write
directly to a profile's **About**, **Experience**, or **Projects** sections — for
any account type:
- The **Profile API is `GET`-only**, and partner-gated even for reads beyond
name/photo/headline. There is no `POST`/`PATCH` for profile fields.
- **Member Data Portability** (the DMA product) is read-only and EU/EEA/Switzerland-only.
- Nothing else in LinkedIn's product catalog writes member profile data.
That's a platform restriction, not something this code can work around without
violating LinkedIn's User Agreement §8.2 — which prohibits "software, devices,
scripts, robots or any other means or processes (such as crawlers, **browser plugins
and add-ons** or any other technology)". A browser extension is named in the same
clause as a headless bot, so it isn't a safer middle ground; the penalty is account
restriction or permanent ban. This project doesn't go there.
| Tool | What it does |
|---|---|
| `get_my_profile` | Reads your name, email, photo via OpenID Connect |
| `create_post` | Publishes a text or link post to your feed |
| `generate_resume_pdf` | Builds a PDF from structured About/Experience/Projects data, formatted for LinkedIn's own **Import Resume** feature |
| `generate_profile_sections` | Length-checks finished profile copy against LinkedIn's real limits (headline 220, About 2600, role description 2000) and says where each piece goes |
| `add_certification_link` | Builds an **Add to Profile** link for a certification — see the caveat below |
`generate_resume_pdf` is the practical route to "updating" Experience/Education:
LinkedIn auto-parses a resume PDF and pre-fills those sections for review. **About**
and **Projects** aren't reliably auto-parsed — `generate_profile_sections` exists to
make that manual paste mechanical rather than a guessing game about lengths.
⚠️ **`add_certification_link` no longer prefills.** LinkedIn's help centre now states
these links "will no longer be customized for a specific certificate or degree" and
"will no longer autofill and members must enter the relevant information directly on
their profile". Many third-party guides and URL builders still advertise the old
prefilling behaviour — they're out of date. The tool still emits the detail parameters
(they cost nothing and would start working again if LinkedIn reverts), but treat it as
a shortcut to the right form, **not** as a profile write.
## Two connection modes, and which credentials each one wants
This server supports two different ways of authenticating, auto-detected per request:
1. **Fixed shared secret** (Postman, curl, ChatGPT's connector) — you generate
`MCP_BEARER_TOKEN` yourself (e.g. `openssl rand -hex 32`) and put it in `.env`
alongside `LINKEDIN_ACCESS_TOKEN` (obtained once via `auth_setup.py`). Clients
send `Authorization: Bearer <your MCP_BEARER_TOKEN>`, and the server calls
LinkedIn using the stored `LINKEDIN_ACCESS_TOKEN` underneath.
2. **Direct LinkedIn OAuth passthrough** (Claude's connector) — Claude logs
into LinkedIn itself using your **LinkedIn app's Client ID/Secret** (not
anything you generate) and sends *LinkedIn's own* resulting access token
as the Bearer header. The server detects this token doesn't match your
`MCP_BEARER_TOKEN` and uses it directly as the LinkedIn access token instead.
Don't mix these up: `MCP_BEARER_TOKEN` is something you invent for mode 1;
your LinkedIn app's Client ID/Secret (from LinkedIn's own Auth tab) are what
mode 2 needs, and they're never typed into `.env` at all — Claude holds them.
## 1. LinkedIn app setup
1. Create an app: https://www.linkedin.com/developers/apps
2. Auth tab → add **two** redirect URLs:
- `http://localhost:8765/callback` (for `auth_setup.py`, mode 1)
- `https://claude.ai/api/mcp/auth_callback` (for Claude's connector, mode 2)
3. Products tab → request:
- **Sign In with LinkedIn using OpenID Connect** (auto-approved)
- **Share on LinkedIn** (auto-approved)
4. Copy the Client ID / Secret from the Auth tab — you'll use these directly
in Claude's connector settings for mode 2, and/or in your own `.env` for
`auth_setup.py` (mode 1).
## 2. Local setup & one-time LinkedIn auth
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# fill in LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, and set a random MCP_BEARER_TOKEN
python auth_setup.py # opens browser, completes LinkedIn OAuth, saves LINKEDIN_ACCESS_TOKEN to .env
```
## 3. Deploy it somewhere public
Both Claude and ChatGPT need to reach this server over HTTPS — `localhost` won't
work for either unless you tunnel it. Pick one:
**Quick test (ngrok tunnel, temporary URL):**
```bash
python server.py # starts on http://0.0.0.0:8000
ngrok http 8000 # gives you a temporary https:// URL
```
**Permanent (Docker, deploy to Render/Fly.io/Railway/any host that runs containers):**
```bash
docker build -t linkedin-mcp .
docker run -p 8000:8000 --env-file .env linkedin-mcp
```
Point that platform's generated HTTPS domain at container port 8000, path `/mcp`.
A `Procfile` is also included for platforms that build directly from source
(Railway, Heroku-style) without a Dockerfile.
**Your MCP endpoint URL will be:** `https://<your-domain>/mcp`
## 4. Connect it to Claude
Claude.ai → Settings → Connectors → Add custom connector. The dialog only has
three fields that matter here — no separate Authorization URL / Token URL
inputs, despite what you might expect from a typical OAuth setup:
- **Name**: whatever you want it labeled as (e.g. `linkedIn-mcp`)
- **Remote MCP server URL**: `https://<your-domain>/mcp`
- **Advanced settings → OAuth Client ID / OAuth Client Secret**: your
**LinkedIn app's** Client ID and Client Secret (from LinkedIn's Auth tab) —
not anything you generate yourself, and not your server's `MCP_BEARER_TOKEN`
Click **Add**, then **Connect**. This sends you to LinkedIn's real login/consent
screen — log in and approve. LinkedIn redirects back to Claude with a genuine
access token, which Claude then sends as the Bearer header on every tool call.
Before this works, add a second authorized redirect URL in your LinkedIn app's
**Auth** tab (alongside `http://localhost:8765/callback`, which `auth_setup.py`
still uses separately):
```
https://claude.ai/api/mcp/auth_callback
```
The server handles the rest automatically: since the token Claude sends won't
match your own `MCP_BEARER_TOKEN`, it's treated as a real LinkedIn access token
and used directly against LinkedIn's API — no code changes needed per person
who connects. This also means multiple people could each connect with their
own LinkedIn account against the same deployed server.
**If Connect fails at the LinkedIn-login step itself** (not after — see below),
double check the redirect URL above was saved exactly, and that both "Sign In
with LinkedIn using OpenID Connect" and "Share on LinkedIn" show as fully
added under your LinkedIn app's Products tab.
**If it reaches LinkedIn's login/consent screen fine, but fails silently right
after you approve:** that's LinkedIn's token endpoint requiring the client
secret in the POST body rather than an `Authorization: Basic` header, which
some OAuth clients don't handle by default. If that happens and persists,
fall back to the Claude Desktop + `mcp-remote` approach below — it sidesteps
this entirely.
## Connecting via Claude Desktop + mcp-remote (guaranteed fallback)
If the direct-OAuth route above doesn't work, or you're not on claude.ai web:
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"linkedin": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://<your-domain>/mcp",
"--header", "Authorization: Bearer YOUR_MCP_BEARER_TOKEN"
]
}
}
}
```
This runs locally as a subprocess and attaches your fixed bearer token directly —
no OAuth negotiation involved. Restart Claude Desktop after saving.
## Connecting it to ChatGPT
ChatGPT → Settings → Apps & Connectors → Advanced settings → enable **Developer
mode** (requires Plus/Pro/Business/Enterprise) → Add custom connector:
- **URL**: `https://<your-domain>/mcp`
- **Authentication**: choose token/API key auth → paste your `MCP_BEARER_TOKEN`
- In a new chat, open the `+` menu → Developer mode → toggle this connector on
for that conversation (ChatGPT enables connectors per-conversation).
## Using `generate_resume_pdf`
```python
generate_resume_pdf(
full_name="Vaibhav Sharma",
headline="Frontend / Full-Stack Developer",
email="you@example.com",
phone="+91-XXXXXXXXXX",
location="Bengaluru, India",
about="Frontend/full-stack developer specializing in AI-powered conversational...",
experience=[{
"title": "Frontend Developer",
"company": "Kapture CRM",
"location": "Bengaluru, India",
"start_date": "Jan 2024",
"end_date": "Present",
"bullets": [
"Built and shipped the Vitos AI voice/chat agent builder UI end-to-end",
"Normalized Redux state using createEntityAdapter across the Vitos state tree",
],
}],
projects=[{
"name": "Shifra",
"description": "Voice-controlled AI assistant built with xAI's Grok API.",
"tech": ["React", "TypeScript", "Grok API", "Vercel"],
}],
skills=["React", "TypeScript", "Redux", "Node.js"],
)
```
The tool returns the PDF as `pdf_base64`, so you get the file back regardless of where
the server runs — ask Claude/ChatGPT to save it for you. `output_path` is optional and
only writes to the *server's* disk, which is useful when running locally and useless
once deployed.
Then: **LinkedIn → Me → View Profile → More → Build a resume → Import from resume**,
review the auto-filled Experience/Education, and paste the About/Projects text manually.
Run `generate_profile_sections` first to confirm that text fits before you paste it.
## Files
- `server.py` — remote MCP server (streamable-http + bearer auth) + tool definitions
- `linkedin_client.py` — LinkedIn REST API wrapper (userinfo, posts)
- `resume_builder.py` — reportlab-based resume PDF generator
- `profile_links.py` — character limits + Add to Profile URL builder (pure, no network)
- `auth_setup.py` — one-time OAuth2 flow to obtain a LinkedIn access token
- `Dockerfile` / `Procfile` — deployment
- `.env.example` — credential templateThis server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues