Clientes OAuth
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
Algunos servidores MCP están protegidos. Envíales una solicitud sin token y responden 401 Unauthorized.
OAuthClientProvider es la forma de conseguir el token. No es un objeto de MCP en absoluto. Es un httpx2.Auth, el hook estándar de httpx2 para "hacer algo con cada solicitud". Lo asocias a un httpx2.AsyncClient, le pasas ese cliente al transporte Streamable HTTP y dejas de pensar en ello.
Esta página es el lado del cliente. Hacer que tu propio servidor exija un token es Autorización.
El proveedor
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])
Le das cuatro cosas:
server_url: el endpoint MCP al que te conectas. El proveedor descubre todo lo demás a partir de él.client_metadata: lo que escribirías en el formulario de "registrar una aplicación" de un servidor de autorización.storage: dónde viven los tokens entre ejecuciones.redirect_handlerycallback_handler: los dos momentos en los que interviene un humano.
Nada más en el archivo menciona OAuth. main() nunca ve un token.
Metadatos del cliente
OAuthClientMetadata es el documento de registro real de RFC 7591, como modelo de Pydantic.
Defines tres campos. Los valores por defecto completan el resto: grant_types ya es ["authorization_code", "refresh_token"] y response_types ya es ["code"], que es exactamente el flujo que ejecuta este proveedor.
Check
Al ser un modelo de Pydantic, valida antes de que un solo byte salga a la red.
Omite redirect_uris y la construcción falla en el acto con un ValidationError que
nombra el campo:
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
No se abre ningún navegador ni queda un registro a medias en el servidor de autorización.
Almacenamiento de tokens
TokenStorage es un Protocol con cuatro métodos asíncronos. No heredas de nada; escribe los métodos y cualquier clase es un almacén de tokens:
get_tokens/set_tokensguardan elOAuthToken: token de acceso, token de actualización, caducidad, scope.get_client_info/set_client_infoguardan elOAuthClientInformationFullque el servidor de autorización emitió cuando el proveedor te registró, incluido tuclient_id.
La versión en memoria de arriba funciona. También olvida todo cuando el proceso termina, así que la siguiente ejecución repite todo el proceso. Persístelo en un archivo o en el llavero de tu plataforma y la siguiente ejecución transcurre en silencio.
Tip
Guarda client_info, no solo los tokens. El proveedor se registra dinámicamente la primera vez que
no encuentra un client_info almacenado. Si lo descartas, generas un registro nuevo en cada ejecución.
Los dos handlers
El flujo de código de autorización necesita un humano exactamente una vez: alguien tiene que iniciar sesión y hacer clic en "permitir".
redirect_handlerse espera con la URL de autorización ya construida por completo. Elclient_id, elredirect_uri, elstatey el desafío PKCE ya están en ella. Tu único trabajo es llevar un navegador hasta allí. Una app de escritorio llama awebbrowser.open; este archivo la imprime.callback_handlerse espera a continuación. Aguarda hasta que el usuario vuelve a turedirect_uriy devuelve los parámetros de consulta de esa redirección como unAuthorizationCodeResult.
Un cliente real ejecuta un pequeño servidor HTTP local en el URI de redirección en lugar de llamar a input(). La forma es idéntica: recibe la redirección y devuelve code, state e iss.
Warning
Pasa state e iss exactamente como llegaron. El proveedor compara state con el que
generó e iss con el emisor que descubrió, y rechaza cualquier discrepancia. Son las defensas
contra CSRF y contra la confusión de servidores.
Dentro del Client
Mira main(). El proveedor va en el cliente httpx2, el cliente httpx2 va en streamable_http_client(url, http_client=...) y ese transporte va en Client.
streamable_http_client no tiene argumento nombrado auth=. Todo lo que es de nivel HTTP (autenticación, cabeceras, timeouts, proxies) pertenece al httpx2.AsyncClient que traes. Esa organización en capas está en Transportes del cliente.
Lo que el proveedor hace por ti
La primera vez que Client envía una solicitud, el servidor responde 401. El proveedor toma el control:
- Descubrimiento. Lee la cabecera
WWW-Authenticate, obtiene los Protected Resource Metadata del servidor desde/.well-known/oauth-protected-resource, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de ese servidor. - Registro. ¿No hay nada en el almacenamiento? Te registra dinámicamente con tu
OAuthClientMetadatay guarda el resultado. - Autorización. Genera el par PKCE y un
state, construye la URL de autorización, espera turedirect_handlery luego espera tucallback_handlerpara obtener el código. - Intercambio. Cambia el código por un
OAuthToken, lo guarda y repite tu solicitud original conAuthorization: Bearer ....
Después de eso, se queda callado. Los tokens salen del almacenamiento, un token de acceso caducado se renueva con el token de actualización y solo cuando nada de eso funciona vuelve a ejecutar el flujo.
No escribiste nada de eso. Quedan dos argumentos nombrados (client_metadata_url y validate_resource_url), y este archivo no necesita ninguno. client_metadata_url es el que vale la pena conocer; tiene su propia sección más abajo.
Pruébalo
La mayoría de los ejemplos de esta documentación puedes comprobarlos con un Client(server) en memoria. Este no: todo el sentido del flujo es un 401 HTTP, y no hay HTTP entre un cliente en memoria y su servidor.
El repositorio incluye la versión real. examples/servers/simple-auth/ ejecuta un servidor de autorización independiente y un servidor MCP protegido; examples/clients/simple-auth-client/ es el cliente de esta página convertido en una pequeña CLI. Su README tiene los dos comandos: inicia los servidores, ejecuta el cliente contra ellos y verás pasar los cuatro pasos.
Client ID Metadata Documents
La revisión 2026-07-28 de la especificación declara obsoleto el registro dinámico de clientes en favor de los Client ID Metadata Documents (CIMD). En lugar de enviar un POST con un registro nuevo a cada servidor de autorización que encuentra, tu cliente publica un único documento JSON sobre sí mismo en una URL HTTPS estable, y esa URL es su client_id. El servidor de autorización obtiene el documento; el proveedor nunca lo toca.
El SDK ya lo admite: pasa la URL como client_metadata_url= al construir el proveedor. Cuando los metadatos del servidor de autorización anuncian client_id_metadata_document_supported: true, el proveedor se salta por completo la solicitud a /register: la URL entra en el flujo como client_id y no hay client_secret. Cuando el servidor no lo anuncia (la mayoría aún no lo hace), o nunca pasas una URL, el proveedor recurre al registro dinámico en silencio, y todo lo anterior funciona exactamente como se describe. Un client_info almacenado sigue teniendo prioridad sobre ambos.
La URL debe ser HTTPS con una ruta que no sea la raíz; cualquier otra cosa es un ValueError en la construcción, antes de que ocurra nada en la red. El ejemplo incluido en examples/clients/simple-auth-client/ la toma de la variable de entorno MCP_CLIENT_METADATA_URL.
De máquina a máquina
Un trabajo nocturno, un paso de CI, otro servicio. No hay navegador ni nadie que haga clic en "permitir". Ese es el grant client credentials: ya tienes un client_id y un client_secret, y el endpoint de token es todo el flujo.
ClientCredentialsOAuthProvider es el mismo httpx2.Auth, sin el humano:
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])
Qué cambió:
- Sin
OAuthClientMetadata, sin handlers. Pasasclient_idyclient_secret; el proveedor construye un registroclient_credentialsmínimo en torno a ellos y se salta el registro dinámico por completo. scopees una cadena separada por espacios, el formato que OAuth usa en lo que se transmite.- Todo lo que viene después es idéntico: el mismo
TokenStorage, el mismohttpx2.AsyncClient(auth=...), el mismostreamable_http_client.
Por defecto, el secreto viaja como autenticación HTTP Basic en la solicitud de token (client_secret_basic). Pasa token_endpoint_auth_method="client_secret_post" para ponerlo en el cuerpo del formulario en su lugar. Algunos servidores de autorización solo aceptan uno de los dos.
Tip
Lee client_secret del entorno o de un gestor de secretos, nunca del control de versiones.
Info
Hay un proveedor más en mcp.client.auth.extensions.client_credentials:
PrivateKeyJWTOAuthProvider, para clientes que se autentican con un JWT en lugar de un
secreto compartido (private_key_jwt, la variante de par de claves e identidad de carga de trabajo). Sigue
el mismo patrón: construye uno y ponlo en auth=. El mismo módulo incluye
SignedJWTParameters y static_assertion_provider, dos utilidades que construyen su aserción.
Hay una situación más sin humanos: el cliente pertenece a una empresa cuyo proveedor de identidad, y no el usuario, decide a qué servidores MCP puede acceder. Ese es un grant distinto, con su propio modelo de confianza y su propia página, Aserción de identidad.
Cuando falla
Cuando el flujo OAuth sale mal, el proveedor lanza un OAuthFlowError de mcp.client.auth. Tiene dos subclases. OAuthRegistrationError significa que el registro no produjo un cliente que puedas usar: el servidor de autorización se negó a registrarte, o sí te registró pero con credenciales que este flujo no puede usar (por ejemplo, un método de autenticación que no implementa). OAuthTokenError significa que no se pudo obtener un token: el endpoint de token dijo que no, o un registro de cliente almacenado lleva un método de autenticación que este cliente no puede aplicar, lo cual se informa al construir la solicitud de token en lugar de enviarse. Un solo except OAuthFlowError: cubre descubrimiento, registro, autorización e intercambio.
No todo es un error de flujo. La red todavía puede fallar; esas son excepciones ordinarias de httpx2 y pasan sin modificar.
Resumen
OAuthClientProvideres unhttpx2.Auth. Ponlo en unhttpx2.AsyncClient, pásaselo astreamable_http_client(url, http_client=...)yClientnunca se entera de que hubo OAuth.- Aportas cuatro cosas: la URL del servidor, un
OAuthClientMetadata, unTokenStoragey el par de handlers de redirección y callback. TokenStoragees unProtocol: cuatro métodos asíncronos, sin clase base. Persisteclient_infoademás de los tokens.- El descubrimiento, el registro (dinámico o mediante un Client ID Metadata Document), PKCE, las comprobaciones de
stateeissy la renovación de tokens son trabajo del proveedor, no tuyo. ClientCredentialsOAuthProvideres la versión sin humanos:client_id+client_secret, sin handlers, sin navegador.- Todo fallo de OAuth es un
OAuthFlowError;OAuthRegistrationErroryOAuthTokenErrorson sus subclases.
La otra mitad de este handshake, hacer que tu servidor exija el token, es Autorización.