OAuth e protocolo para clientes próprios | Desenvolvedores Steel
OAuth e protocolo para clientes próprios
Para quem escreve um cliente MCP. Descoberta, registro dinâmico, autorização com PKCE, troca e renovação de tokens, revogação e as chamadas JSON-RPC ao servidor, com exemplos de requisição e resposta.
Nesta página
Claude, ChatGPT, Cursor e os SDKs oficiais de MCP fazem tudo isto sozinhos:
basta o endereço do servidor. Esta página é para quem escreve um cliente
próprio, depura uma integração ou quer entender o que acontece por baixo. Os
exemplos usam o ambiente de homologação; troque pelo endereço do seu Steel.
O servidor de autorização é o do Steel, OAuth 2.1:
Item
Valor
Recurso protegido
https://homologacao.stratustelecom.com.br/api/mcp
Emissor (issuer)
o endereço do Steel seguido de /api/auth
Fluxo
Authorization code com PKCE (S256, obrigatório)
Registro de cliente
Dinâmico (RFC 7591), sem autenticação; clientes registrados assim são públicos (token_endpoint_auth_method: none)
Escopos
mcp:read, mcp:write e offline_access (para receber refresh token)
Access token
Opaco, prefixo stl_at_, vale 1 hora
Refresh token
Prefixo stl_rt_, vale 30 dias, só com offline_access
Código de autorização
Vale 10 minutos, uso único
1. Descoberta
Uma chamada sem token ao servidor MCP responde 401 com o cabeçalho
WWW-Authenticate apontando para os metadados do recurso:
E os do servidor de autorização (RFC 8414). O Steel responde no caminho do
RFC (/.well-known/oauth-authorization-server/api/auth) e no caminho curto,
com o mesmo documento:
A resposta é 201. Registrar não dá acesso a nada: quem decide é a pessoa, na
tela de consentimento. Guarde o client_id e reutilize; não registre de novo a
cada login.
Como o registro é aberto, o client_name é só o que o app diz de si. A tela de
consentimento identifica o app pelo host do redirect_uri desta
autorização (lido da query assinada pelo provedor, que já conferiu o
redirect_uri com os registrados):
redirect_uri
Título da tela
Destino mostrado
HTTPS em claude.ai, claude.com, chatgpt.com, openai.com, cursor.com, cursor.sh (ou subdomínio), ou o esquema cursor://
O nome da lista do Steel (Claude, ChatGPT, Cursor) e o selo Verificado
O host
Loopback: localhost, 127.0.0.0/8, [::1]
O client_name ("Meu cliente MCP quer acessar o seu Steel")
um app no seu computador e o host:porta
Qualquer outro
Um app em host quer acessar o seu Steel e o aviso App não verificado, com o client_name entre aspas ("Ele diz chamar-se “…”")
O host
O host vai em ASCII: domínio internacional aparece em punycode (xn--…),
userinfo não conta (https://claude.ai@exemplo.com é exemplo.com) e porta
fora do padrão aparece. Uma query com assinatura inválida nem chega a ser
mostrada. O host também fica gravado na concessão (redirectHost em
GET /api/users/me/integration-grants) e aparece em Apps conectados ao
lado do nome.
Cliente de terminal: use um redirect_uri de loopback. Cliente web fora da
lista é identificado pelo host, não pelo client_name, então use um domínio
que a pessoa reconheça.
Sem redirect_uris, o registro responde 400 invalid_redirect_uri.
3. Autorização com PKCE
Gere um code_verifier aleatório (43 a 128 caracteres) e o code_challenge
(SHA-256 dele em base64url):
GET /api/auth/oauth2/authorize?response_type=code&client_id=kXq2…&redirect_uri=http%3A%2F%2F127.0.0.1%3A8976%2Fcallback&scope=mcp%3Aread%20mcp%3Awrite%20offline_access&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256&resource=https%3A%2F%2Fhomologacao.stratustelecom.com.br%2Fapi%2Fmcp
Host: homologacao.stratustelecom.com.br
O que a pessoa vê:
Sem sessão no Steel, a tela de login; depois, volta sozinha.
A tela Meu cliente MCP quer acessar o seu Steel, com Depois de
permitir, você volta para um app no seu computador (127.0.0.1:8976),
onde escolhe o Workspace, os Módulos e o Nível de acesso (Somente ler ou
Ler e alterar), e clica em Permitir acesso ou Recusar. Se o
scope pedido não tiver mcp:write, só Somente ler fica disponível.
O navegador vai para o redirect_uri com code, state e iss:
Confira se o state é o que você gerou e se o iss é o emissor esperado.
Recusado, o retorno traz error=access_denied.
O parâmetro resource (RFC 8707) é o endereço do servidor MCP. O Steel aceita
também o emissor ou o endereço do Steel, para clientes que mandam um desses.
O scope devolvido é o que a pessoa concedeu, não o que você pediu: quem
escolheu Somente ler gera um token sem mcp:write. O workspace e os
módulos não aparecem no token; eles ficam na concessão, do lado do Steel.
Erro
Quando
invalid_grant
Código vencido (10 minutos), já usado, redirect_uri diferente ou code_verifier que não confere
invalid_client
client_id desconhecido
invalid_request
Faltou um parâmetro
5. Renovação
Antes de a hora do access token acabar, ou depois de um 401 com
error="invalid_token":
A resposta tem o mesmo formato da troca. Guarde o refresh_token novo quando
ele vier. Um refresh que falha com invalid_grant quer dizer que a conexão foi
revogada, a pessoa saiu do workspace ou passaram os 30 dias: recomece pela
autorização.
Revoga o token enviado. Cliente público (registro dinâmico) revoga só com o
client_id, sem segredo — o documento de descoberta anuncia none em
revocation_endpoint_auth_methods_supported.
Revogar o refresh token encerra a conexão: ele leva junto os access tokens
emitidos com ele e, quando não sobra nenhum token vivo daquela concessão, ela
sai de Apps conectados na hora. É o que um logout do cliente deve fazer.
Revogar só um access token, com o refresh token ainda válido, mantém a
conexão. Um refresh token reutilizado (já trocado por outro) também derruba a
família inteira e a conexão some da lista. Conexões em que todos os tokens
venceram saem da lista na limpeza da madrugada.
Para encerrar a conexão pelo Steel, a pessoa usa Configurações › Segurança ›
Apps conectados; um administrador, Ajustes › Apps conectados. Nos dois
casos, a próxima chamada do cliente responde 401.
7. Falar MCP
O transporte é Streamable HTTP sem estado: cada mensagem JSON-RPC é um
POST independente, com Authorization: Bearer e
Accept: application/json, text/event-stream. Não há sessão MCP no servidor;
GET e DELETE respondem 405.
A resposta traz serverInfo, as capacidades (tools, resources,
prompts) e as instructions do servidor, um texto em português que explica
as regras (permissões do usuário, escrita, confirmUrl, idempotencyKey) e
vale a pena repassar ao modelo.
O primeiro content é um resumo para o modelo ler; o segundo, os mesmos dados
em JSON, para clientes que não leem structuredContent. Uma recusa do Steel
volta como resultado com isError: true, a mensagem em português e o código
em structuredContent.error.code, não como erro JSON-RPC. Um nome de
ferramenta que a conexão não alcança responde o erro JSON-RPC -32602.
resources/list, resources/read, prompts/list e prompts/get funcionam
do mesmo jeito; veja Resources e prompts.
Limites
Cada pessoa tem 120 chamadas a cada 60 segundos ao servidor MCP, e cada cliente
OAuth, 600 a cada 60 segundos somando todas as pessoas. Passou, a resposta é
429 com Retry-After em segundos.
Solução de problemas
Sintoma
O que fazer
400 invalid_redirect_uri no registro
Mande pelo menos um redirect_uris
A autorização é recusada antes da tela de consentimento
Faltou o PKCE: envie code_challenge com code_challenge_method=S256 (plain não é aceito), ou o redirect_uri não é um dos registrados
invalid_grant na troca
O código passou de 10 minutos, já foi usado, ou o code_verifier/redirect_uri não é o mesmo da autorização
Sem refresh_token na resposta
Peça offline_access no scope e inclua refresh_token em grant_types no registro
401 com error="invalid_token"
O access token passou de 1 hora (renove) ou a conexão foi revogada (recomece)
As ferramentas de escrita não aparecem
O token não tem mcp:write (a pessoa escolheu Somente ler) ou o administrador não ligou Permitir escrita via MCP
405 no servidor
O cliente tentou GET (SSE do protocolo antigo). Use POST
A resposta chega toda no fim
Algum proxy entre o cliente e o Steel faz buffering de text/event-stream