Clientes OAuth
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Alguns servidores MCP são protegidos. Envie a eles uma requisição sem token e a resposta é 401 Unauthorized.
OAuthClientProvider é como você consegue o token. Ele não é um objeto MCP. É um httpx2.Auth, o hook padrão do httpx2 para "fazer algo em toda requisição". Você o anexa a um httpx2.AsyncClient, entrega esse cliente ao transporte Streamable HTTP e para de pensar no assunto.
Esta página é o lado do cliente. Fazer o seu próprio servidor exigir um token está em Autorização.
O provider
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])
Você entrega quatro coisas a ele:
server_url: o endpoint MCP ao qual você está se conectando. O provider descobre todo o resto a partir dele.client_metadata: o que você digitaria no formulário "registrar uma aplicação" de um servidor de autorização.storage: onde os tokens ficam entre uma execução e outra.redirect_handlerecallback_handler: os dois momentos em que um humano participa.
Nada mais no arquivo menciona OAuth. main() nunca vê um token.
Metadados do cliente
OAuthClientMetadata é o documento de registro real da RFC 7591, na forma de um modelo Pydantic.
Você define três campos. Os valores padrão preenchem o resto: grant_types já é ["authorization_code", "refresh_token"] e response_types já é ["code"], que é exatamente o fluxo que este provider executa.
Check
Por ser um modelo Pydantic, ele valida antes de um único byte trafegar pela rede.
Deixe redirect_uris de fora e a construção falha na hora com um ValidationError que
nomeia o campo:
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
Nenhum navegador aberto, nenhum registro pela metade deixado para trás no servidor de autorização.
Armazenamento de tokens
TokenStorage é um Protocol com quatro métodos async. Você não herda de nada; escreva os métodos e qualquer classe vira um armazenamento de tokens:
get_tokens/set_tokensguardam oOAuthToken: token de acesso, refresh token, expiração, escopo.get_client_info/set_client_infoguardam oOAuthClientInformationFullque o servidor de autorização emitiu quando o provider registrou você, incluindo o seuclient_id.
A versão em memória acima funciona. Ela também esquece tudo quando o processo termina, então a próxima execução refaz a dança inteira. Persista em um arquivo ou no keyring da sua plataforma e a próxima execução fica silenciosa.
Tip
Armazene client_info, não só os tokens. O provider faz o registro dinâmico na primeira vez em que
não encontra um client_info armazenado. Jogue-o fora e você cria um registro novo a cada execução.
Os dois handlers
O fluxo de authorization code precisa de um humano exatamente uma vez: alguém tem que fazer login e clicar em "permitir".
redirect_handlerrecebe um await com a URL de autorização já montada. Oclient_id, aredirect_uri, ostatee o desafio PKCE já estão nela. Seu único trabalho é levar um navegador até lá. Um app desktop chamawebbrowser.open; este arquivo imprime a URL.callback_handlerrecebe o await em seguida. Ele espera até o usuário voltar para a suaredirect_urie retorna os parâmetros de query desse redirecionamento como umAuthorizationCodeResult.
Um cliente real executa um pequeno servidor HTTP local na URI de redirecionamento em vez de chamar input(). O formato é idêntico: receber o redirecionamento, devolver code, state e iss.
Warning
Repasse state e iss exatamente como chegaram. O provider compara state com o que
ele gerou e iss com o issuer que descobriu, e recusa qualquer divergência. Eles são as defesas
contra CSRF e contra confusão de servidor (server mix-up).
Para dentro do Client
Veja main(). O provider vai no cliente httpx2, o cliente httpx2 vai em streamable_http_client(url, http_client=...), e esse transporte vai em Client.
streamable_http_client não tem o parâmetro nomeado auth=. Tudo que é de nível HTTP (auth, cabeçalhos, timeouts, proxies) pertence ao httpx2.AsyncClient que você traz. Essa divisão em camadas está em Transportes do cliente.
O que o provider faz por você
Na primeira vez que Client envia uma requisição, o servidor responde 401. O provider assume:
- Descoberta. Ele lê o cabeçalho
WWW-Authenticate, busca os Protected Resource Metadata do servidor em/.well-known/oauth-protected-resource, descobre qual servidor de autorização protege este recurso e busca os metadados desse servidor. - Registro. Nada no armazenamento? Ele registra você dinamicamente com o seu
OAuthClientMetadatae armazena o resultado. - Autorização. Ele gera o par PKCE e um
state, monta a URL de autorização, faz await no seuredirect_handlere depois faz await no seucallback_handlerpara obter o code. - Troca. Ele troca o code por um
OAuthToken, armazena e reenvia a sua requisição original comAuthorization: Bearer ....
Depois disso ele fica quieto. Os tokens saem do armazenamento, um token de acesso expirado é renovado com o refresh token, e só quando nada disso funciona ele executa o fluxo de novo.
Você não escreveu nada disso. Restam dois argumentos nomeados (client_metadata_url e validate_resource_url), e este arquivo não precisa de nenhum dos dois. client_metadata_url é o que vale a pena conhecer; ele ganha uma seção própria abaixo.
Experimente
A maioria dos exemplos nesta documentação você consegue conferir com um Client(server) em memória. Este não: o ponto central do fluxo é um 401 HTTP, e não há HTTP entre um cliente em memória e o seu servidor.
O repositório traz a versão ao vivo. examples/servers/simple-auth/ executa um servidor de autorização independente e um servidor MCP protegido; examples/clients/simple-auth-client/ é o cliente desta página crescido até virar uma pequena CLI. O README dele tem os dois comandos: inicie os servidores, execute o cliente contra eles e veja as quatro etapas passarem.
Client ID Metadata Documents
A revisão 2026-07-28 da especificação torna obsoleto o registro dinâmico de clientes em favor dos Client ID Metadata Documents (CIMD). Em vez de fazer POST de um registro novo em cada servidor de autorização que encontra, o seu cliente publica um único documento JSON sobre si mesmo em uma URL HTTPS estável, e essa URL é o client_id dele. O servidor de autorização busca o documento; o provider nunca toca nele.
O SDK já fala isso: passe a URL como client_metadata_url= ao construir o provider. Quando os metadados do servidor de autorização anunciam client_id_metadata_document_supported: true, o provider pula completamente a requisição a /register: a URL entra no fluxo como client_id, e não há client_secret. Quando o servidor não anuncia isso (a maioria ainda não anuncia), ou você nunca passa uma URL, o provider recorre ao registro dinâmico silenciosamente, e tudo acima funciona exatamente como descrito. Um client_info armazenado ainda prevalece sobre ambos.
A URL precisa ser HTTPS com um caminho que não seja a raiz; qualquer outra coisa é um ValueError na construção, antes de qualquer tráfego de rede. O examples/clients/simple-auth-client/ do repositório recebe a URL pela variável de ambiente MCP_CLIENT_METADATA_URL.
Máquina para máquina
Um job noturno, uma etapa de CI, outro serviço. Não há navegador nem ninguém para clicar em "permitir". Esse é o grant client credentials: você já possui um client_id e um client_secret, e o endpoint de token é o fluxo inteiro.
ClientCredentialsOAuthProvider é o mesmo httpx2.Auth, sem o 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])
O que mudou:
- Sem
OAuthClientMetadata, sem handlers. Você passaclient_ideclient_secret; o provider monta um registroclient_credentialsmínimo em torno deles e pula o registro dinâmico por completo. scopeé uma string separada por espaços, o formato OAuth usado na comunicação.- Tudo a partir daí é idêntico: o mesmo
TokenStorage, o mesmohttpx2.AsyncClient(auth=...), o mesmostreamable_http_client.
Por padrão, o secret viaja como HTTP Basic auth na requisição de token (client_secret_basic). Passe token_endpoint_auth_method="client_secret_post" para colocá-lo no corpo do formulário. Alguns servidores de autorização só aceitam um dos dois.
Tip
Leia client_secret do ambiente ou de um gerenciador de segredos, nunca do controle de versão.
Info
Mais um provider mora em mcp.client.auth.extensions.client_credentials:
PrivateKeyJWTOAuthProvider, para clientes que se autenticam com um JWT em vez de um
segredo compartilhado (private_key_jwt, a variante de par de chaves e workload identity). Ele segue
o mesmo padrão: construa um, coloque em auth=. O mesmo módulo traz
SignedJWTParameters e static_assertion_provider, dois helpers que montam a assertion dele.
Há mais uma situação sem humano: o cliente pertence a uma empresa cujo provedor de identidade, e não o usuário, decide quais servidores MCP ele pode alcançar. Esse é um grant diferente, com seu próprio modelo de confiança e sua própria página, Asserção de identidade.
Quando falha
Quando o fluxo OAuth dá errado, o provider levanta um OAuthFlowError de mcp.client.auth. Ele tem duas subclasses. OAuthRegistrationError significa que o registro não rendeu um cliente que você possa usar: o servidor de autorização se recusou a registrar você, ou até registrou, mas com credenciais que este fluxo não consegue usar (por exemplo, um método de autenticação que ele não implementa). OAuthTokenError significa que não foi possível obter um token: o endpoint de token disse não, ou um registro de cliente armazenado carrega um método de autenticação que este cliente não consegue aplicar, o que é reportado durante a montagem da requisição de token em vez de ser enviado. Um único except OAuthFlowError: cobre descoberta, registro, autorização e troca.
Nem tudo é erro de fluxo. A rede ainda pode falhar; essas são exceções comuns do httpx2 e passam intactas.
Recapitulando
OAuthClientProvideré umhttpx2.Auth. Coloque-o em umhttpx2.AsyncClient, passe esse cliente parastreamable_http_client(url, http_client=...), eClientnunca fica sabendo que houve OAuth.- Você fornece quatro coisas: a URL do servidor, um
OAuthClientMetadata, umTokenStoragee o par de handlers redirect/callback. TokenStorageé umProtocol: quatro métodos async, sem classe base. Persistaclient_infoalém dos tokens.- Descoberta, registro (dinâmico ou via um Client ID Metadata Document), PKCE, as verificações de
stateeisse a renovação de tokens são trabalho do provider, não seu. ClientCredentialsOAuthProvideré a versão sem humano:client_id+client_secret, sem handlers, sem navegador.- Toda falha OAuth é um
OAuthFlowError;OAuthRegistrationErroreOAuthTokenErrorsão suas subclasses.
A outra metade desse handshake, fazer o seu servidor exigir o token, está em Autorização.