OAuth 2.0 pour Serveurs MCP
Mis à jour le 29 juillet 2026
Pourquoi OAuth dans MCP ?
Tant que votre serveur MCP se contente de convertir des devises ou d’interroger une base publique, la question de l’identité ne se pose pas. Elle devient centrale dès que le serveur touche à des emails, à un calendrier, à des dépôts GitHub ou à des tickets Linear : il ne travaille plus pour lui-même, il agit au nom d’un utilisateur précis. Un token statique posé dans un fichier de configuration ne dit rien de ce consentement. Il ne dit pas qui a autorisé quoi, ni pour combien de temps, ni jusqu’où portent les droits accordés. OAuth 2.0 répond exactement à ces trois manques : consentement explicite, droits limités par des scopes, renouvellement sécurisé.
Rappelez-vous les trois niveaux d’authentification vus précédemment. Le premier, aucune auth, convient à une URL publique sans données sensibles. Le deuxième, token statique, correspond à une clé API générée manuellement, comme un Personal Access Token GitHub. Le troisième, OAuth 2.0, ajoute le flux de consentement complet avec scopes et refresh tokens. Le passage du deuxième au troisième niveau n’est pas une question de sophistication technique mais de responsabilité : à partir du moment où plusieurs personnes utilisent votre serveur sur leurs propres données, le token partagé devient un trou de sécurité.
Le déroulé du flux
Quand un utilisateur connecte un serveur MCP avec OAuth dans Le Chat, sept étapes s’enchaînent, et il vaut la peine de les visualiser une fois pour toutes :
1. L'utilisateur clique "Connecter" dans Le Chat
↓
2. Le Chat redirige vers la page d'autorisation du service
(ex: GitHub, Google, Linear)
↓
3. L'utilisateur accorde les permissions demandées (scopes)
↓
4. Le service renvoie un code d'autorisation au callback
↓
5. Le client échange le code contre un access token + refresh token
↓
6. L'access token est utilisé pour chaque appel MCP
↓
7. Quand l'access token expire, le refresh token en obtient un nouveau
Trois jetons différents circulent dans cette chorégraphie et on les confond souvent. L’authorization code est temporaire et ne sert qu’une seule fois : il est échangé immédiatement contre les vrais tokens. L’access token accompagne ensuite chaque requête et vit peu de temps, typiquement une heure. Le refresh token, enfin, sert uniquement à obtenir un nouvel access token sans redemander son accord à l’utilisateur. Cette durée de vie courte de l’access token est une protection : si un token fuite, sa fenêtre d’exploitation reste étroite.
Configurer OAuth côté client Python
Le SDK Mistral outille ce flux avec le helper build_oauth_params et quelques méthodes du client MCP :
from mistralai.extra.mcp.auth import build_oauth_params
from mistralai.extra.mcp.streamable_http import (
MCPClientStreamableHTTP,
StreamableHTTPServerParams,
)
REDIRECT_URL = "http://localhost:16010/oauth/callback"
mcp_client = MCPClientStreamableHTTP(
params=StreamableHTTPServerParams(url="https://mcp.linear.app/mcp", timeout=100)
)
async def connecter():
if not await mcp_client.requires_auth():
return # Le serveur est ouvert : rien à faire
# 1. Découvrir les endpoints OAuth du serveur
# (et enregistrer dynamiquement le client si aucun client_id fourni)
oauth_params = await build_oauth_params(
mcp_client.base_url,
redirect_url=REDIRECT_URL,
)
mcp_client.set_oauth_params(oauth_params=oauth_params)
# 2. Construire l'URL de consentement et l'ouvrir dans le navigateur
login_url, state = await mcp_client.get_auth_url_and_state(REDIRECT_URL)
print(f"Autorisez l'accès : {login_url}")
# 3. Récupérer le callback, puis échanger le code contre un token
auth_response = await attendre_le_callback() # petit serveur HTTP local
token = await mcp_client.get_token_from_auth_response(
auth_response["url"],
redirect_url=REDIRECT_URL,
state=state,
)
mcp_client.set_auth_token(token)
build_oauth_params fait deux choses et deux seulement : il lit les métadonnées OAuth publiées par le serveur MCP, et il enregistre dynamiquement un client si vous ne lui passez ni client_id ni client_secret. Le reste du flux vous appartient — ouvrir l’URL de consentement, recueillir le callback sur le port de votre redirect_url, échanger le code contre un token. Le SDK ne masque pas ces étapes, il les outille, et c’est précisément ce découpage qui vous permet de câbler le callback dans une application web plutôt que dans un script.
Le moindre privilège, appliqué aux scopes
Les scopes définissent ce que votre serveur pourra faire une fois l’accord donné, et l’écran de consentement les affiche à l’utilisateur. Demander large est tentant : cela évite de revenir en arrière si un besoin apparaît. C’est aussi le meilleur moyen de faire fuir un utilisateur prudent et d’exposer inutilement son compte. Comparez :
# Mauvais : trop de permissions
scope=["repo", "admin:org", "delete_repo", "user"]
# Bon : juste ce qu'il faut pour créer des issues
scope=["repo:issues", "read:user"]
Un serveur qui crée des issues n’a aucune raison de pouvoir supprimer des dépôts ou administrer l’organisation. Chaque service publie sa propre nomenclature de scopes : consultez la documentation OAuth du service cible plutôt que de deviner par analogie.
Stocker et renouveler les tokens
Les tokens obtenus valent les identifiants qu’ils remplacent, et méritent le même soin. Écrivez-les dans un fichier dédié dont vous restreignez les permissions système :
import os
import json
from pathlib import Path
TOKEN_FILE = Path.home() / ".mcp" / "tokens.json"
def save_tokens(service: str, access_token: str, refresh_token: str):
"""Sauvegarde les tokens de manière sécurisée."""
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
tokens = {}
if TOKEN_FILE.exists():
tokens = json.loads(TOKEN_FILE.read_text())
tokens[service] = {
"access_token": access_token,
"refresh_token": refresh_token,
}
TOKEN_FILE.write_text(json.dumps(tokens))
# Restreindre les permissions du fichier
os.chmod(TOKEN_FILE, 0o600)
Le chmod en 0o600 n’est pas décoratif : sans lui, sur une machine partagée, n’importe quel compte lit vos jetons. Le renouvellement, lui, se réduit à un appel POST sur le token_url du service avec le refresh token en main. L’utilisateur ne voit rien passer, ce qui est précisément l’intérêt :
import httpx
async def refresh_access_token(
token_url: str,
client_id: str,
client_secret: str,
refresh_token: str,
) -> dict:
"""Renouvelle l'access token via le refresh token."""
async with httpx.AsyncClient() as http:
response = await http.post(
token_url,
data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret,
},
)
response.raise_for_status()
return response.json()
Quand un simple token suffit
Tous les serveurs ne justifient pas cette machinerie. Pour un outil interne consommé par une poignée de développeurs, une clé API transmise dans un header HTTP fait très bien l’affaire, et le client MCP l’accepte directement :
from mistralai.extra.mcp.streamable_http import (
MCPClientStreamableHTTP,
StreamableHTTPServerParams,
)
# Serveur avec token simple
mcp_client = MCPClientStreamableHTTP(
params=StreamableHTTPServerParams(
url="https://mon-serveur.com/mcp",
headers={"Authorization": "Bearer mon_api_token_secret"},
timeout=60,
)
)
Côté serveur, la vérification tient dans un middleware qui intercepte les routes concernées avant qu’elles n’atteignent votre logique métier :
from fastapi import FastAPI, Request, HTTPException
api = FastAPI()
@api.middleware("http")
async def verify_token(request: Request, call_next):
if request.url.path.startswith("/mcp"):
token = request.headers.get("Authorization", "")
if token != "Bearer mon_api_token_secret":
raise HTTPException(status_code=401, detail="Non autorisé")
return await call_next(request)
Ce que Le Chat gère pour vous
Dans Le Chat, les connecteurs featured — GitHub, Gmail, Linear — arrivent avec OAuth déjà câblé : l’utilisateur clique, consent, et c’est terminé. Pour vos connecteurs custom, la configuration vous propose les trois mêmes options que celles décrites plus haut : No authentication pour une URL publique, Token si vous collez la clé à la main, et OAuth 2.0 pour le flux complet. Un détail a des conséquences pratiques importantes : le token est stocké par utilisateur, pas par organisation. Dans une équipe Mistral, chaque membre passe donc par son propre consentement et travaille avec ses propres credentials — ce qui évite qu’un départ ou une révocation ne casse l’accès de tout le monde.
Points clés à retenir
- OAuth 2.0 permet un consentement explicite avec des droits limités (scopes)
- Le flux : autorisation → code → tokens (access + refresh)
- Appliquez le principe du moindre privilège sur les scopes
- Les tokens doivent être stockés de manière sécurisée et renouvelés automatiquement
- Pour les cas simples, un token statique dans les headers HTTP suffit
- Dans Le Chat, les tokens sont stockés par utilisateur