Skip to main content
Glama
Steph-ux

linkedin-mcp-server

by Steph-ux
README.md
# LinkedIn MCP Server

Serveur MCP (Model Context Protocol) pour piloter LinkedIn depuis un agent IA (Claude Code, Claude Desktop, Manus, etc.). Repose sur Patchright (fork anti-détection de Playwright) et un profil Chrome persistant pour conserver une session authentifiée tout en bypassant les protections Arkose/CAPTCHA.

---

## ⚠️ Breaking change & durcissement (sécurité)

> **Les écritures sont désactivées par défaut.** Toutes les actions qui modifient
> LinkedIn (post, message, connexion, candidature, follow, react, etc. — 43
> fonctions) refusent de s'exécuter tant que la variable d'environnement
> **`LINKEDIN_MCP_ENABLE_WRITES=1`** n'est pas posée. C'est un garde-fou
> **structurel** contre l'injection de prompt indirecte : ni le contenu scrapé
> ni l'agent ne peuvent le contourner.

```bash
# Pour autoriser les écritures (à poser côté config MCP, env du process) :
LINKEDIN_MCP_ENABLE_WRITES=1
```

Autres durcissements appliqués :

| Domaine | Avant | Après |
|---|---|---|
| **Prompt injection** | descriptions / messages / notifs renvoyés en `innerText` brut | `_sanitize_external()` strip les caractères de contrôle/zéro-largeur et neutralise les phrases impératives ; contenu externe encapsulé via `_wrap_untrusted()` |
| **Soumission de candidature** | champs inconnus remplis avec `"3"` (devinette) | plus aucune devinette : champs non reconnus laissés vides + **refus de soumettre** s'il en reste (à renseigner via `answers_json`) |
| **Captcha / checkpoint** | `detect_checkpoint()` jamais appelé (code mort) | navigation via `_goto()` qui combine `retry_async()` (retry transitoire) + `detect_checkpoint()` sur les lectures clés |
| **Nettoyage Chrome** | `taskkill /f /im chrome.exe` (tuait **tout** Chrome) | kill ciblé **uniquement** sur le profil automatisé (`~/.linkedin-mcp/profile`) via `psutil`, repli PowerShell filtré par ligne de commande |
| **Traçabilité erreurs** | `except Exception` muets (0 traceback) | `traceback.format_exc()` loggé en stderr sur 60+ handlers |
| **Double encodage JSON** | dispatcher `manage_jobs` ré-encodait du JSON déjà sérialisé | dispatch retourne directement la chaîne JSON |
| **Limite résultats** | `search_jobs` figé à `jobs[:5]` | paramètre **`limit`** exposé (défaut 25) |

> 💡 `psutil` est recommandé pour le kill ciblé. S'il est absent, un repli
> PowerShell (Windows) / `pkill -f <profil>` (Unix) est utilisé.

---

## 🌟 Fonctionnalités

**10 outils consolidés** :

| # | Outil | Couverture |
|---|---|---|
| 1 | `manage_profile_info` | Profil : lecture, headline, about, photo, bannière, analytics |
| 2 | `manage_profile_sections` | Expériences et formations (ajout/suppression, dates, "poste actuel") |
| 3 | `manage_skills` | Compétences : add / delete / endorse |
| 4 | `manage_recommendations` | Recommandations : demander / rédiger |
| 5 | `manage_network` | Connexions, follow, block, recherche simple + avancée |
| 6 | `manage_messaging` | DMs, pièces jointes, InMails, non-lus, notifications |
| 7 | `manage_publications` | Posts, médias, sondages, articles, carrousels, événements, programmation |
| 8 | `engage_social` | React / commenter / repost / save |
| 9 | `manage_groups_and_pages` | Publier sur page entreprise ou groupe, join/leave, analytics page |
| 10 | `manage_jobs` | Recherche, détails, save, Easy Apply, candidature externe (Greenhouse/Lever) |

---

## ⚙️ Installation

```bash
pip install -r requirements.txt
patchright install chromium
```

Dépendances pinnées (`requirements.txt`) :

```
mcp>=1.0.0,<2.0.0
patchright>=1.40.0,<2.0.0
python-dotenv>=1.0.0,<2.0.0
```

---

## 🔑 Authentification (bypass anti-bot)

LinkedIn bloque Playwright standard via Arkose. La procédure ci-dessous ouvre un Chrome réel piloté en mode isolé pour passer outre :

```bash
python cleanup_and_login.py
```

Ce script :

1. Tue les Chrome résiduels et purge les profils corrompus
2. Ouvre une fenêtre Chrome propre
3. Tu te connectes à LinkedIn (coche **"Se souvenir de moi"**)
4. Attends 5 s sur le feed → le script détecte la session et la sauvegarde dans `~/.linkedin-mcp/profile`

Cette étape est à refaire uniquement si la session expire (rare) ou si LinkedIn force une reconnexion.

---

## 🛠️ Enregistrement du serveur MCP

### Claude Code (recommandé)

```bash
claude mcp add linkedin --scope user -- \
  "C:/Users/<user>/AppData/Local/Programs/Python/Python312/python.exe" \
  "C:/Users/<user>/Downloads/LinkedIn MCP Server/linkedin_mcp_server.py"
```

Vérification :

```bash
claude mcp list | grep linkedin
# → linkedin: ... - ✓ Connected
```

> ⚠️ **Cold-start requis** : après `claude mcp add`, ferme et relance Claude Code (un `--resume` ne re-découvre pas les nouveaux MCP tools).

### Claude Desktop / Manus

`claude_desktop_config.json` :

```json
{
  "mcpServers": {
    "linkedin": {
      "command": "C:/Users/<user>/AppData/Local/Programs/Python/Python312/python.exe",
      "args": ["C:/Users/<user>/Downloads/LinkedIn MCP Server/linkedin_mcp_server.py"]
    }
  }
}
```

---

## 🔐 Données personnelles — scraping automatique

**Aucun `.env` à remplir pour les PII.** Les champs nom / email / téléphone / localisation sont scrapés directement depuis ton compte LinkedIn connecté :

| Champ | Source |
|---|---|
| `first_name`, `last_name`, `full_name` | `https://www.linkedin.com/in/<toi>/` (H1 du profil) |
| `location` | Top card du profil |
| `email`, `phone` | `https://www.linkedin.com/mypreferences/d/category/account` |

Le résultat est mis en cache par process : un seul scrape au premier appel d'un outil qui en a besoin, gratuit ensuite.

**Fallback optionnel** : si LinkedIn change la page settings (rare) et que le scrape email/phone échoue, tu peux poser ces variables dans `~/.linkedin-mcp/.env` — elles seront utilisées en dernier recours, champ par champ :

```
LINKEDIN_EMAIL=...
LINKEDIN_PHONE=...
```

### Salaire — calculé par l'IA

Le salaire **n'est jamais stocké** côté serveur. L'agent IA doit le calculer en fonction des compétences, du niveau et du poste, puis le passer via le paramètre `salary_expectation` du tool `manage_jobs` (action `easy_apply`).

---

## 📚 Référence des outils

### 1. `manage_profile_info`
Profil : informations de base, visuels, stats.
* **Actions** : `get`, `get_analytics`, `edit_headline`, `edit_about`, `update_picture`, `update_banner`
* **Args** : `action` (req), `profile_url`, `headline`, `about`, `image_path`

### 2. `manage_profile_sections`
Expériences et formations.
* **Actions** : `add_experience`, `delete_experience`, `add_education`, `delete_education`
* **Args** : `action` (req), `title` / `company` (req pour expérience), `school` / `degree` (req pour formation), `field_of_study`, `start_month`, `start_year`, `end_month`, `end_year`, `current` (bool), `location`, `employment_type`, `description`

### 3. `manage_skills`
* **Actions** : `add`, `delete`, `endorse`
* **Args** : `action` (req), `skill_name` (req), `profile_url` (req pour `endorse`)

### 4. `manage_recommendations`
* **Actions** : `request`, `write`
* **Args** : `action` (req), `recipient_url_or_name` (req), `relationship`, `position`, `text`

### 5. `manage_network`
* **Actions** : `connect`, `accept`, `remove`, `withdraw`, `follow`, `unfollow`, `block`, `unblock`, `search`, `search_advanced`
* **Args** : `action` (req), `profile_url_or_name`, `note`, `password` (pour `unblock`), `keywords`, `location`, `network` (`first`/`second`/`third`), `company`, `industry`, `school`, `limit` (défaut 10)

### 6. `manage_messaging`
* **Actions** : `send`, `send_attachment`, `send_inmail`, `get_unread`, `read_thread`, `get_notifications`
* **Args** : `action` (req), `recipient_url_or_name`, `message`, `subject` (pour `send_inmail`), `file_path` (pour `send_attachment`), `max_messages` (défaut 50)

### 7. `manage_publications`
* **Actions** : `post_simple`, `post_media`, `post_mentions`, `post_poll`, `schedule_post`, `post_article`, `post_carousel`, `trending_hashtags`, `create_event`
* **Args** : `action` (req), `content`, `title`, `visibility` (`public`/`connections`), `media_paths`, `mentions`, `poll_question`, `poll_options`, `poll_duration_days`, `schedule_year/month/day/hour/minute`, `pdf_path`, `cover_image_path`, `event_name`, `event_type` (`ONLINE`/`IN_PERSON`), `start_date`, `end_date`, `start_time`, `end_time`, `timezone`, `external_link`, `location`

### 8. `engage_social`
* **Actions** : `react`, `comment`, `repost`, `save_post`
* **Args** : `action` (req), `post_url` (req), `reaction` (`LIKE`/`CELEBRATE`/...), `comment_text`, `repost_thoughts`

### 9. `manage_groups_and_pages`
* **Actions** : `post_page`, `post_group`, `join_group`, `leave_group`, `get_company_analytics`
* **Args** : `action` (req), `target_url_or_handle` (req), `content`

### 10. `manage_jobs`
* **Actions** : `search`, `details`, `save_job`, `easy_apply`, `external_apply`
* **Args** :
  * `action` (req)
  * `keywords`, `location`, `easy_apply`, **`limit`** (nombre max de résultats, défaut 25) (pour `search`)
  * `job_id_or_url`
  * `answers_json` (JSON de réponses pré-définies pour `easy_apply`)
  * `submit` (bool, défaut `False` — quand `False` le serveur s'arrête sur la review et sauvegarde une capture dans `~/.linkedin-mcp/screenshots/easy_apply_review.png`)
  * `resume_path`, `answers_dict` (pour `external_apply`)
  * **`salary_expectation`** (str, optionnel) — montant que l'IA doit calculer en fonction du poste / niveau / compétences

---

## 🎯 Visibilité des publications

| Valeur | Description |
| :--- | :--- |
| `public` | Visible par tout le monde (défaut LinkedIn) |
| `connections` | Visible uniquement par tes connexions |

---

## 📁 Structure du projet

```
.
├── README.md
├── CLAUDE.md                    ← contexte pour sessions Claude Code
├── linkedin_mcp_server.py       ← serveur MCP monolithique
├── cleanup_and_login.py         ← procédure d'auth (1 fois)
├── requirements.txt
├── tests/                       ← suites d'intégration (LinkedIn live requis)
├── tools/                       ← scripts utilitaires (debug, inspection, dumps)
└── scratch/                     ← captures et artefacts de debug
```

---

## 🧪 Tests d'intégration

**Tests unitaires hors-ligne (recommandés, aucune session requise)** — vérifient
les correctifs de durcissement de façon déterministe (deps lourdes stubbées) :

```bash
python tests/test_hardening.py        # runner autonome → "18 passed, 0 failed"
# ou
pytest tests/test_hardening.py -q
```

Couverture : fix d'URL (`_normalize_job_url`), garde-fou d'écriture
(`_writes_enabled` / `_assert_write_allowed`), sanitisation anti-injection
(`_sanitize_external` / `_wrap_untrusted`), suppression du fallback `"3"`,
câblage du code anciennement mort.

**Tests d'intégration live** (session LinkedIn active requise) :

```bash
python tests/test_linkedin_new_features.py
python tests/full_test_suite.py
```

---

## 🔒 Sécurité

* **Aucun identifiant en clair** : les cookies sont stockés par Chrome dans `~/.linkedin-mcp/profile`, isolés du Chrome principal.
* **Pas de PII dupliquée** : nom/email/téléphone/localisation viennent de LinkedIn lui-même, pas d'un fichier de config à maintenir.
* **Logging stderr-safe** : `print` est redirigé sur `stderr` pour ne pas polluer le canal stdio JSON-RPC du MCP.
* **Pas d'injection JS** : tous les `page.evaluate()` passent les valeurs via args, pas via f-string.
* **Écritures désactivées par défaut** : 43 fonctions de modification protégées par `LINKEDIN_MCP_ENABLE_WRITES=1` (garde-fou structurel anti prompt-injection).
* **Sanitisation du contenu externe** : `_sanitize_external()` / `_wrap_untrusted()` neutralisent les tentatives d'injection venant des descriptions, messages et notifications scrapés.
* **Détection captcha/checkpoint** : `_goto()` (retry + `detect_checkpoint`) stoppe proprement si LinkedIn sert une vérification, plutôt que de renvoyer des données douteuses.
* **Kill Chrome ciblé** : seul le profil automatisé est terminé — les autres fenêtres Chrome de l'utilisateur ne sont jamais tuées.
* **Usage responsable** : respecte les limites LinkedIn (anti-spam, anti-scraping). Évite les boucles rapides massives.