Clients OAuth
Traduction automatique
Cette page a été traduite automatiquement à partir de la documentation en anglais, et la page en anglais fait foi. Si quelque chose vous semble incorrect, la page Traductions explique comment le signaler.
Certains serveurs MCP sont protégés. Envoyez-leur une requête sans jeton et ils répondent 401 Unauthorized.
OAuthClientProvider est le moyen d’obtenir ce jeton. Ce n’est pas du tout un objet MCP. C’est un httpx2.Auth, le hook standard de httpx2 pour « faire quelque chose à chaque requête ». Vous l’attachez à un httpx2.AsyncClient, vous confiez ce client au transport Streamable HTTP, et vous n’y pensez plus.
Cette page couvre le côté client. Pour que votre propre serveur exige un jeton, voyez Autorisation.
Le fournisseur
from urllib.parse import parse_qs, urlparse
import httpx2
from pydantic import AnyUrl
from mcp import Client
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
class InMemoryTokenStorage:
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
async def open_browser(authorization_url: str) -> None:
print(f"Visit: {authorization_url}")
async def wait_for_callback() -> AuthorizationCodeResult:
redirect_url = input("Paste the URL you were redirected to: ")
params = parse_qs(urlparse(redirect_url).query)
return AuthorizationCodeResult(
code=params["code"][0],
state=params["state"][0],
iss=params["iss"][0] if "iss" in params else None,
)
oauth = OAuthClientProvider(
server_url="http://localhost:8001/mcp",
client_metadata=OAuthClientMetadata(
client_name="Bookshop Agent",
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
scope="user",
),
storage=InMemoryTokenStorage(),
redirect_handler=open_browser,
callback_handler=wait_for_callback,
)
async def main() -> None:
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Vous lui donnez quatre choses :
server_url: le point de terminaison MCP auquel vous vous connectez. Le fournisseur découvre tout le reste à partir de lui.client_metadata: ce que vous saisiriez dans le formulaire « enregistrer une application » d’un serveur d’autorisation.storage: là où les jetons vivent entre deux exécutions.redirect_handleretcallback_handler: les deux moments où un humain intervient.
Rien d’autre dans le fichier ne mentionne OAuth. main() ne voit jamais un jeton.
Métadonnées du client
OAuthClientMetadata est le véritable document d’enregistrement de la RFC 7591, sous forme de modèle Pydantic.
Vous définissez trois champs. Les valeurs par défaut remplissent le reste : grant_types vaut déjà ["authorization_code", "refresh_token"] et response_types vaut déjà ["code"], ce qui correspond exactement au flux qu’exécute ce fournisseur.
Check
Comme c’est un modèle Pydantic, il valide avant qu’un seul octet ne parte sur le réseau.
Omettez redirect_uris et la construction échoue immédiatement avec une ValidationError qui
nomme le champ :
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
Aucun navigateur ouvert, aucun enregistrement à moitié terminé laissé derrière sur le serveur d’autorisation.
Stockage des jetons
TokenStorage est un Protocol avec quatre méthodes asynchrones. Vous n’héritez de rien ; écrivez les méthodes et n’importe quelle classe devient un magasin de jetons :
get_tokens/set_tokensconservent l’objetOAuthToken: jeton d’accès, jeton d’actualisation, expiration, portée.get_client_info/set_client_infoconservent l’objetOAuthClientInformationFullque le serveur d’autorisation a émis lorsque le fournisseur vous a enregistré, y compris votreclient_id.
La version en mémoire ci-dessus fonctionne. Elle oublie aussi tout quand le processus se termine, si bien que l’exécution suivante refait toute la procédure. Persistez-la dans un fichier ou dans le trousseau de votre plateforme et l’exécution suivante est silencieuse.
Tip
Stockez client_info, pas seulement les jetons. Le fournisseur s’enregistre dynamiquement la première fois qu’il
ne trouve aucun client_info stocké. Jetez-le et vous créez un nouvel enregistrement à chaque exécution.
Les deux gestionnaires
Le flux du code d’autorisation a besoin d’un humain exactement une fois : quelqu’un doit se connecter et cliquer sur « autoriser ».
redirect_handlerest attendu (await) avec l’URL d’autorisation entièrement construite. Leclient_id, leredirect_uri, lestateet le défi PKCE y figurent déjà. Votre seul travail est d’y amener un navigateur. Une application de bureau appellewebbrowser.open; ce fichier l’affiche.callback_handlerest attendu ensuite. Il patiente jusqu’à ce que l’utilisateur revienne sur votreredirect_uriet renvoie les paramètres de requête de cette redirection sous la forme d’unAuthorizationCodeResult.
Un vrai client fait tourner un petit serveur HTTP local sur l’URI de redirection au lieu d’appeler input(). La forme est identique : recevoir la redirection, rendre code, state et iss.
Warning
Transmettez state et iss exactement tels qu’ils sont arrivés. Le fournisseur compare state à celui
qu’il a généré et iss à l’émetteur qu’il a découvert, et refuse toute divergence. Ce sont les défenses
contre le CSRF et contre la confusion de serveurs (mix-up).
Dans le Client
Regardez main(). Le fournisseur va sur le client httpx2, le client httpx2 va dans streamable_http_client(url, http_client=...), et ce transport va dans Client.
streamable_http_client n’a pas de mot-clé auth=. Tout ce qui relève du niveau HTTP (authentification, en-têtes, délais d’expiration, proxys) appartient au httpx2.AsyncClient que vous apportez. Cette superposition de couches est décrite dans Transports client.
Ce que le fournisseur fait pour vous
La première fois que Client envoie une requête, le serveur répond 401. Le fournisseur prend le relais :
- Découverte. Il lit l’en-tête
WWW-Authenticate, récupère les Protected Resource Metadata du serveur depuis/.well-known/oauth-protected-resource, apprend quel serveur d’autorisation protège cette ressource, et récupère les métadonnées de ce serveur-là. - Enregistrement. Rien dans le stockage ? Il vous enregistre dynamiquement avec votre
OAuthClientMetadataet stocke le résultat. - Autorisation. Il génère la paire PKCE et un
state, construit l’URL d’autorisation, attend votreredirect_handler, puis attend votrecallback_handlerpour obtenir le code. - Échange. Il échange le code contre un
OAuthToken, le stocke, et rejoue votre requête d’origine avecAuthorization: Bearer ....
Après cela, il se fait discret. Les jetons sortent du stockage, un jeton d’accès expiré est actualisé avec le jeton d’actualisation, et ce n’est que lorsque rien de tout cela ne fonctionne qu’il relance le flux.
Vous n’avez rien écrit de tout cela. Il reste deux arguments nommés (client_metadata_url et validate_resource_url), et ce fichier n’a besoin d’aucun des deux. client_metadata_url est celui qui mérite d’être connu ; il a sa propre section plus bas.
Essayer
La plupart des exemples de cette documentation se vérifient avec un Client(server) en mémoire. Pas celui-ci : tout l’intérêt du flux est un 401 HTTP, et il n’y a pas de HTTP entre un client en mémoire et son serveur.
Le dépôt fournit la version réelle. examples/servers/simple-auth/ exécute un serveur d’autorisation autonome et un serveur MCP protégé ; examples/clients/simple-auth-client/ est le client de cette page devenu une petite CLI. Son README donne les deux commandes : démarrez les serveurs, lancez le client contre eux, et vous voyez défiler les quatre étapes.
Client ID Metadata Documents
La révision 2026-07-28 de la spécification rend obsolète l’enregistrement dynamique des clients au profit des Client ID Metadata Documents (CIMD). Au lieu d’envoyer par POST un nouvel enregistrement à chaque serveur d’autorisation qu’il rencontre, votre client publie un unique document JSON le décrivant à une URL HTTPS stable, et cette URL est son client_id. Le serveur d’autorisation récupère le document ; le fournisseur n’y touche jamais.
Le SDK le parle déjà : passez l’URL dans client_metadata_url= quand vous construisez le fournisseur. Lorsque les métadonnées du serveur d’autorisation annoncent client_id_metadata_document_supported: true, le fournisseur saute entièrement la requête /register : l’URL entre dans le flux en tant que client_id, et il n’y a pas de client_secret. Lorsque le serveur ne l’annonce pas (la plupart ne le font pas encore), ou que vous ne passez jamais d’URL, le fournisseur se rabat silencieusement sur l’enregistrement dynamique, et tout ce qui précède fonctionne exactement comme décrit. Un client_info stocké l’emporte toujours sur les deux.
L’URL doit être en HTTPS avec un chemin autre que la racine ; tout le reste lève une ValueError à la construction, avant le moindre échange réseau. L’exemple fourni examples/clients/simple-auth-client/ la reçoit via la variable d’environnement MCP_CLIENT_METADATA_URL.
De machine à machine
Une tâche nocturne, une étape de CI, un autre service. Il n’y a pas de navigateur et personne pour cliquer sur « autoriser ». C’est le type d’octroi client credentials : vous détenez déjà un client_id et un client_secret, et le point de terminaison de jeton constitue tout le flux.
ClientCredentialsOAuthProvider est le même httpx2.Auth, l’humain en moins :
import httpx2
from mcp import Client
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
class InMemoryTokenStorage:
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
oauth = ClientCredentialsOAuthProvider(
server_url="http://localhost:8001/mcp",
storage=InMemoryTokenStorage(),
client_id="reporting-agent",
client_secret="...",
scope="user",
)
async def main() -> None:
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
Ce qui a changé :
- Aucun
OAuthClientMetadata, aucun gestionnaire. Vous passezclient_idetclient_secret; le fournisseur construit autour d’eux un enregistrementclient_credentialsminimal et saute entièrement l’enregistrement dynamique. scopeest une chaîne séparée par des espaces, le format qu’OAuth utilise sur la liaison.- Tout ce qui se trouve en aval est identique : le même
TokenStorage, le mêmehttpx2.AsyncClient(auth=...), le mêmestreamable_http_client.
Par défaut, le secret voyage en authentification HTTP Basic sur la requête de jeton (client_secret_basic). Passez token_endpoint_auth_method="client_secret_post" pour le placer plutôt dans le corps du formulaire. Certains serveurs d’autorisation n’acceptent que l’une des deux méthodes.
Tip
Lisez client_secret depuis l’environnement ou un gestionnaire de secrets, jamais depuis le contrôle de version.
Info
Un fournisseur de plus se trouve dans mcp.client.auth.extensions.client_credentials :
PrivateKeyJWTOAuthProvider, pour les clients qui s’authentifient avec un JWT plutôt qu’avec un
secret partagé (private_key_jwt, la variante à paire de clés et identité de charge de travail). Il suit
le même schéma : construisez-en un, placez-le sur auth=. Le même module fournit
SignedJWTParameters et static_assertion_provider, deux utilitaires qui construisent son assertion.
Il existe une autre situation sans humain : le client appartient à une entreprise dont le fournisseur d’identité, et non l’utilisateur, décide quels serveurs MCP il peut atteindre. C’est un type d’octroi différent, avec son propre modèle de confiance et sa propre page, Assertion d’identité.
En cas d’échec
Quand le flux OAuth tourne mal, le fournisseur lève une OAuthFlowError depuis mcp.client.auth. Elle a deux sous-classes. OAuthRegistrationError signifie que l’enregistrement n’a pas produit un client utilisable : le serveur d’autorisation a refusé de vous enregistrer, ou il vous a bien enregistré mais avec des identifiants que ce flux ne peut pas utiliser (par exemple une méthode d’authentification qu’il n’implémente pas). OAuthTokenError signifie qu’un jeton n’a pas pu être obtenu : le point de terminaison de jeton a dit non, ou une fiche client stockée porte une méthode d’authentification que ce client ne peut pas appliquer, ce qui est signalé pendant la construction de la requête de jeton plutôt qu’envoyé. Un seul except OAuthFlowError: couvre la découverte, l’enregistrement, l’autorisation et l’échange.
Tout n’est pas une erreur de flux. Le réseau peut toujours échouer ; ce sont des exceptions httpx2 ordinaires et elles passent sans être modifiées.
Récapitulatif
OAuthClientProviderest unhttpx2.Auth. Placez-le sur unhttpx2.AsyncClient, passez celui-ci àstreamable_http_client(url, http_client=...), etClientne sait jamais qu’OAuth a eu lieu.- Vous fournissez quatre choses : l’URL du serveur, un
OAuthClientMetadata, unTokenStorageet la paire de gestionnaires redirect/callback. TokenStorageest unProtocol: quatre méthodes asynchrones, pas de classe de base. Persistezclient_infoen plus des jetons.- La découverte, l’enregistrement (dynamique, ou via un Client ID Metadata Document), PKCE, les vérifications de
stateetiss, et l’actualisation des jetons sont l’affaire du fournisseur, pas la vôtre. ClientCredentialsOAuthProviderest la version sans humain :client_id+client_secret, pas de gestionnaires, pas de navigateur.- Tout échec OAuth est une
OAuthFlowError;OAuthRegistrationErroretOAuthTokenErroren sont les sous-classes.
L’autre moitié de cette poignée de main, faire en sorte que votre serveur exige le jeton, se trouve dans Autorisation.