li-mcp-vercel
by jacobenligne
README.md
# li-mcp-vercel
Serveur MCP LinkedIn déployé sur Vercel, avec **proxy OAuth stateless** intégré. Donne
accès au profil personnel (et, après approbation Community Management, aux pages
LinkedIn administrées) depuis Claude ou tout client MCP standard.
## Pourquoi un proxy OAuth?
LinkedIn ne peut pas faire face à un client MCP directement (vérifié en production,
2026-07-27) :
| Exigence MCP (OAuth 2.1) | LinkedIn |
|---|---|
| PKCE (S256) | ❌ non supporté |
| Enregistrement dynamique de client (RFC 7591) | ❌ non supporté |
| Métadonnées AS (RFC 8414) | ❌ 404 — seule la découverte OIDC existe |
| Auth au token endpoint | `client_secret_post` **uniquement** |
Ce serveur est donc **le serveur d'autorisation** aux yeux du client MCP, et parle le
dialecte LinkedIn derrière :
```
Client MCP ──OAuth 2.1 (PKCE + DCR)──▶ ce serveur ──client_secret_post──▶ LinkedIn
```
### Stateless de bout en bout
Aucune base de données, aucun token stocké :
- **client_id** délivré par `/api/oauth/register` = blob HMAC-signé contenant les
`redirect_uris` du client. `/api/oauth/authorize` vérifie contre le blob lui-même.
- **state** traversant LinkedIn = blob signé portant le contexte du client
(redirect_uri, state, code_challenge).
- **code d'autorisation** remis au client = blob signé enveloppant le code LinkedIn,
en attente de la vérification PKCE à `/api/oauth/token`.
- La clé HMAC est dérivée de `LINKEDIN_CLIENT_SECRET` — pas de variable supplémentaire.
Régénérer le secret invalide simplement les flux en vol.
### Endpoints
| Route | Rôle |
|---|---|
| `/api/mcp` | endpoint MCP (transport HTTP) |
| `/.well-known/oauth-protected-resource` | RFC 9728 — désigne ce serveur comme AS |
| `/.well-known/oauth-authorization-server` | RFC 8414 — annonce PKCE + DCR |
| `/api/oauth/register` | RFC 7591 stateless |
| `/api/oauth/authorize` | valide client + PKCE, redirige vers LinkedIn |
| `/api/oauth/callback` | rappelé par LinkedIn, renvoie le code enveloppé au client |
| `/api/oauth/token` | vérifie PKCE, échange chez LinkedIn en `client_secret_post` |
## Modèle de sécurité
Le serveur ne stocke aucun token. Le token LinkedIn de l'appelant est vérifié à chaque
requête puis retransmis à l'API LinkedIn : un appelant n'atteint que les données que son
compte peut déjà voir. Vérification en trois temps (`lib/auth.ts`) :
1. **Introspection** — `POST /oauth/v2/introspectToken`. Présenter nos propres
credentials fait office de contrôle d'audience : un token émis pour une autre app
revient `active: false`.
2. **Scopes** — au moins un scope produit. L'introspection renvoie les scopes séparés
par des **virgules**, pas des espaces.
3. **Identité** — `/v2/userinfo` fournit le `sub` (pairwise, stable par app), filtré par
`ALLOWED_SUBS`.
## Pièges LinkedIn (appris en production)
- **Introspection éventuellement cohérente** : un token fraîchement émis peut lire
`status=revoked` pendant quelques secondes. `/api/oauth/token` retient le token
jusqu'à confirmation `active`, et la vérification réessaie avant de rejeter — sinon le
client jette le token au premier 401.
- **Scopes non accordés = « Bummer, something went wrong »** : n'annoncer dans
`SCOPES_SUPPORTED` que les scopes des produits réellement approuvés. Les scopes org
(palier 2) n'y entrent qu'après approbation Community Management.
- **Découverte** : seul `https://www.linkedin.com/oauth/.well-known/openid-configuration`
existe côté LinkedIn — mais le client MCP n'en a plus besoin, il ne voit que ce serveur.
## Installation
### 1. App LinkedIn ([developer.linkedin.com](https://developer.linkedin.com))
- Produits requis : **Sign In with LinkedIn using OpenID Connect** + **Share on LinkedIn**
- *Authorized redirect URLs* : `https://<ton-deploiement>/api/oauth/callback`
(c'est le **proxy** que LinkedIn rappelle, pas le client MCP)
### 2. Variables d'environnement
| Variable | Rôle |
|---|---|
| `LINKEDIN_CLIENT_ID` | identifiant de l'app (non sensible) |
| `LINKEDIN_CLIENT_SECRET` | secret de l'app — introspection, échange de code, clé HMAC |
| `ALLOWED_SUBS` | `sub` OIDC autorisés, séparés par des virgules |
```bash
vercel env add LINKEDIN_CLIENT_ID production
vercel env add LINKEDIN_CLIENT_SECRET production
vercel env add ALLOWED_SUBS production
```
**Amorçage de `ALLOWED_SUBS`** : laisser vide, se connecter une fois, relever le `sub`
journalisé (`[auth] ALLOWED_SUBS vide — Amorcer avec: …`), le renseigner, redéployer.
Ne pas laisser vide en régime permanent — le contrôle d'identité est alors désactivé.
### 3. Déployer
```bash
npm install
vercel --prod
```
### 4. Connecter depuis Claude
Ajouter un connecteur personnalisé avec la seule URL :
```
https://<ton-deploiement>/api/mcp
```
**Ne pas saisir de client id/secret** — l'enregistrement dynamique s'en charge.
## Outils
### Palier 1 — self-serve
| Outil | Rôle |
|---|---|
| `li_token_status` | validité, expiration, scopes réels du token |
| `li_me` | profil du membre authentifié |
### Palier 2 — après approbation Community Management
Non encore implémenté (posts, pages, statistiques d'organisation).
## Développement local
```bash
npm install
cp .env.local.example .env.local # remplir LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET
npm run dev # http://localhost:3000, endpoint /api/mcp
```
Pour tester le flux OAuth complet en local, ajouter aussi dans l'app LinkedIn
(*Authorized redirect URLs*) :
```
http://localhost:3000/api/oauth/callback
```
Puis brancher un client MCP sur `http://localhost:3000/api/mcp` :
```bash
# Claude Code
claude mcp add --transport http linkedin http://localhost:3000/api/mcp
# ou l'inspecteur MCP
npx @modelcontextprotocol/inspector http://localhost:3000/api/mcp
```
Laisser `ALLOWED_SUBS` vide dans `.env.local` au premier essai (amorçage), puis le
renseigner avec le `sub` journalisé dans la console.
Vérifications rapides sans client :
```bash
curl -s localhost:3000/.well-known/oauth-authorization-server | jq
curl -s -X POST localhost:3000/api/oauth/register \
-H "Content-Type: application/json" \
-d '{"redirect_uris":["http://localhost:6274/oauth/callback"]}' | jq
npm run build # avant tout déploiement
```
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues