Aller au contenu
IA — MCP — Agent IA — Agentic

Erreurs OAuth d'un serveur MCP : causes et correctifs (+ 5 commandes curl pour diagnostiquer)

Les erreurs OAuth d'un serveur MCP (invalid_client_metadata, Unexpected token '<', 421...) : causes, correctifs et 5 commandes curl.

Erreurs OAuth d'un serveur MCP : causes et correctifs

Coder un serveur MCP conforme est la moitié facile du travail. La moitié difficile : chaque client (Claude Code, Codex, Gemini CLI, Copilot CLI, Antigravity...) découvre, enregistre et authentifie légèrement différemment, et chaque différence remonte sous forme d'erreur OAuth générique, depuis une couche que vous n'avez pas écrite. Voici les erreurs que j'ai rencontrées en déployant des serveurs MCP distants, avec leur cause réelle et leur correctif, puis la méthode en cinq commandes curl qui sépare un bug serveur d'un bug client en une minute.

La chaîne de découverte OAuth d'un serveur MCP

Toutes les erreurs ci-dessous cassent un maillon de cette chaîne, définie par la spécification MCP Authorization :

  1. Le client appelle le serveur sans token.
  2. Le serveur répond 401 avec un header WWW-Authenticate qui pointe vers son Protected Resource Metadata (RFC 9728).
  3. Ce document désigne le serveur d'autorisation.
  4. Le client applique la découverte RFC 8414 sur cet identifiant pour trouver les endpoints authorize, token et register.

Erreurs de découverte

SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON

Cause : le client a demandé un document de métadonnées OAuth (/.well-known/oauth-authorization-server ou /.well-known/openid-configuration) et a reçu une page HTML en 200. Typiquement, le catch-all d'un frontend SPA ou une page de login répond sur ce chemin.

Certains clients ne suivent pas le challenge 401 : ils sondent l'origine du serveur, prennent la première réponse qui n'est pas en 4xx et la parsent. Trois rapports de bug distincts se sont révélés être ce seul fait.

Correctif : répondre 404 sur les chemins /.well-known/* que vous ne servez pas. Un 404 vaut mieux qu'un 200 que vous n'aviez pas prévu : le client passe au candidat suivant.

ZodError sur jwks_uri ("path": ["jwks_uri"], "message": "Required")

ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": ["jwks_uri"],
    "message": "Required"
  }
]

Cause : le client a parsé vos métadonnées OAuth avec le schéma OpenID Connect, qui exige jwks_uri. Vous servez /.well-known/openid-configuration alors que votre serveur est un serveur OAuth 2.0 qui n'émet pas d'ID token.

Correctif : ne servez pas openid-configuration si vous n'êtes pas un fournisseur OpenID. Servir un document que vous ne pouvez pas honorer est pire que ne rien servir.

404 sur l'URL resource_metadata annoncée

Cause : le préfixe de chemin est doublé. L'URL de base contient déjà /mcp, et le code y ajoute à nouveau le chemin (/.well-known/oauth-protected-resource/mcp/mcp).

Correctif : comparer l'URL du header WWW-Authenticate avec celle réellement servie (commande 1 et 2 plus bas).

403 en HTML sur /register ou /.well-known/... (Cloud Run)

Cause : sur Cloud Run, le binding IAM allUsers → roles/run.invoker a été perdu, souvent après un redéploiement sans --allow-unauthenticated. C'est Google Front End qui répond, en HTML, avant votre code.

Correctif :

gcloud run services add-iam-policy-binding my-mcp-server --region europe-west1 \
  --member="allUsers" --role="roles/run.invoker"

Votre serveur gère lui-même l'authentification OAuth : il doit être joignable sans IAM. Réaffirmez ce binding dans votre CI pour que le cas ne se reproduise pas.

Erreurs d'enregistrement et de scopes

invalid_client_metadata: Requested scopes are not valid: email, profile

Cause : le client s'enregistre (Dynamic Client Registration) avec l'orthographe courte des scopes Google (email, profile), alors que votre serveur n'accepte que la forme canonique (https://www.googleapis.com/auth/userinfo.email). Le fournisseur considère les deux comme équivalentes, et votre bibliothèque normalise ce que vous configurez, mais pas ce que le client demande.

Correctif : déclarer les deux orthographes dans les scopes acceptés à l'enregistrement.

invalid_scope: Client was not registered with scope email

Cause : même famille, autre porte. Le client s'est enregistré sans scope, a reçu par défaut la forme canonique, puis demande email au moment de l'autorisation. C'est le grant stocké pour ce client qui est vérifié à /authorize.

Correctif : élargir la liste des scopes accordés par défaut, en plus de celle des scopes acceptés. J'ai corrigé ce même symptôme deux fois, une fois par porte : élargissez les deux en même temps.

invalid_client_metadata sans nom de champ

Cause possible : application_type, que la révision 2026-07-28 impose aux clients qui utilisent encore Dynamic Client Registration. Un serveur qui valide strictement les métadonnées d'enregistrement le rejette sans dire pourquoi.

Correctif : accepter explicitement application_type dans la validation.

redirect_uri_mismatch ou invalid_redirect_uri

Cause : deux allowlists distinctes peuvent produire cette erreur :

  • celle du fournisseur d'identité (Google, Entra ID...) : l'URI de callback de votre serveur (https://mon-serveur/auth/callback) n'est pas déclarée sur le client OAuth amont, ou diffère d'un caractère de l'URL de base configurée ;
  • celle de votre serveur : l'URI de redirection du client MCP (loopback avec port aléatoire, URL de connecteur d'un éditeur) n'est pas autorisée.

Correctif : pour le fournisseur, aligner exactement l'URL de base et l'URI déclarée (y compris http://localhost:<port>/auth/callback pour les tests locaux). Pour votre serveur, autoriser en wildcard uniquement ce que le client fait réellement varier (le port loopback, l'id de connecteur) et épingler le reste.

Erreurs de transport et de protocole

405 Method Not Allowed sur GET /mcp

Cause : aucune. C'est attendu : le transport Streamable HTTP stateless n'enregistre que POST, et la révision 2026-07-28 a retiré l'endpoint GET du transport.

UnsupportedProtocolVersionError

Cause : la version du protocole envoyée dans _meta n'est pas acceptée par le serveur.

Correctif : interroger server/discover (commande 5 plus bas) avant de toucher au client ou au serveur. Un serveur qui ne répond pas à server/discover est antérieur à la révision 2026-07-28, ce qui explique souvent le comportement d'un client récent.

421 Misdirected Request

Cause : la protection contre le DNS rebinding de la bibliothèque MCP rejette le header Host reçu, typiquement derrière un load balancer ou un domaine personnalisé.

Correctif : déclarer les hôtes autorisés dans la configuration de sécurité du transport de votre bibliothèque, plutôt que de désactiver la protection.

307 Temporary Redirect vers http:// (slash final)

Cause : le client est configuré avec /mcp/ et le serveur redirige vers /mcp, mais en http:// : derrière un load balancer qui termine TLS, le serveur ASGI ne fait pas confiance au header X-Forwarded-Proto. Le client suit la redirection en clair, ou la refuse.

Correctif : faire confiance aux headers du proxy (forwarded_allow_ips avec uvicorn), et documenter une seule forme d'URL.

Le correctif est déployé, mais le client renvoie la même erreur

Cause : le client a mis en cache un client_id enregistré avant votre correctif. Le grant vérifié à /authorize est celui stocké pour ce client_id : il ne changera jamais. C'est la panne qui m'a fait perdre le plus de temps.

Correctif : vérifier d'abord le serveur avec un enregistrement neuf (curl -X POST .../register), puis vider le cache OAuth du client :

codex mcp logout my-server                 # Codex
rm ~/.copilot/mcp-oauth-config/<hash>.json # Copilot CLI (et le .verifier associé)
# Claude Code : /mcp puis "Clear authentication" sur le serveur

Diagnostiquer en 5 commandes curl

Sans aucun identifiant, ces cinq commandes disent si le problème est chez vous ou chez le client :

B=https://example.com

# 1. le challenge : doit contenir resource_metadata
curl -sD- -o /dev/null -X POST $B/mcp/ -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | grep -i www-authenticate

# 2. le Protected Resource Metadata qu'il désigne
curl -s $B/.well-known/oauth-protected-resource/mcp/ | jq .

# 3. RFC 8414 sur le serveur d'autorisation désigné : l'issuer DOIT correspondre
curl -s $B/.well-known/oauth-authorization-server/mcp | jq '.issuer, .token_endpoint'

# 4. ce que voient les clients qui sondent l'origine : 404 ou JSON, jamais du HTML
curl -sL -o /dev/null -w '%{http_code} %{content_type}\n' \
  $B/.well-known/oauth-authorization-server

# 5. les versions et capacités annoncées (révision 2026-07-28)
curl -s -X POST $B/mcp/ -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' | jq .

Si les cinq réponses sont propres, arrêtez de modifier votre serveur et allez lire le code du client.

Connecter chaque client MCP

Chaque CLI diffère assez pour qu'un snippet copié-modifié soit faux. Vérifiez avec --help avant de documenter :

claude mcp add my-server https://example.com/mcp/ --transport http   # Claude Code, puis /mcp
codex mcp add my-server --url https://example.com/mcp/               # Codex : --url
codex mcp login my-server                                            # et un login explicite
copilot mcp add my-server https://example.com/mcp/ --transport http  # Copilot CLI
gemini mcp add --transport http my-server https://example.com/mcp/   # Gemini CLI, puis /mcp auth
// Antigravity : configuration par fichier uniquement (~/.gemini/config/mcp_config.json)
{"mcpServers": {"my-server": {"serverUrl": "https://example.com/mcp/"}}}

Méthode de debug

  • Lire le code source du client plutôt que son message d'erreur. Trois échecs avaient une seule cause, trouvée en vingt minutes dans la fonction qui construit l'URL de découverte du bundle JavaScript du client.
  • Reproduire avant de corriger. Écrire l'assertion qui échoue, et la voir échouer pour la bonne raison.
  • Séparer "pas déployé" de "toujours cassé". Vérifier la révision qui sert le trafic.
  • Quand un correctif "ne marche pas", suspecter le cache client avant de rouvrir le code.
  • Chercher où ailleurs la même incohérence s'applique, comme les deux portes de scopes.

Conclusion

Prévoyez plus de temps pour la compatibilité des clients que pour le serveur lui-même. La plupart de ces erreurs ne viennent pas de votre code OAuth, mais d'un écart entre ce que votre serveur publie et ce qu'un client précis attend. Les cinq commandes curl tranchent la question en une minute.

7 pannes de serveur MCP qui ressemblent à des bugs OAuth
invalid_client intermittent, clients déconnectés, 500 sur /authorize, 403 Cloud Run : 7 pannes de serveur MCP en production et leur vraie cause.

Dernier