E15 · Três chamadas
5 minRode o bloco tokens.
Pronto quando: HTTP/1.1 401 Unauthorized sem token; com o de leitura, reservar_vaga: recusado (Insufficient scope: required "turmas:reservar"); com o de reserva, reservar_vaga: reservado.
Aula 6 · 20 min
Servidor HTTP na internet sem token é ferramenta aberta para qualquer um. No MCP o seu servidor é um resource server do OAuth: ele não emite token, só confere. Nesta aula você protege o servidor do kit, vê o 401 que inicia o login do cliente e dá a cada token só o escopo que ele precisa.
Começar a aula
Objetivo: proteger o servidor HTTP com token: 401 com metadados, escopo mínimo por ferramenta e segredo fora do repositório.
Clique em cada etapa.
No SDK v2 2.3.1, a validação de iss e a credencial presa ao emissor são opções do lado do cliente (finishAuth); o servidor é o resource server.
Ensaio: dois tokens vêm de variáveis de ambiente. Em produção, troque o verificador pela checagem do JWT do seu IdP.
// http-auth.mjs — o mesmo servidor por HTTP, agora como "resource server" OAuth:
// sem token → 401 com WWW-Authenticate; token sem o escopo → 403 insufficient_scope.
// ENSAIO: os tokens vêm de variáveis de ambiente (nunca do repositório). Em produção quem
// emite o token é o seu servidor de autorização (IdP) e aqui você verifica a assinatura
// do JWT (ou faz introspecção RFC 7662), conferindo exp e aud.
import { createServer as criarHttp } from 'node:http';
import { timingSafeEqual } from 'node:crypto';
import {
createMcpHandler, requireBearerAuth, oauthMetadataResponse,
getOAuthProtectedResourceMetadataUrl, OAuthError, OAuthErrorCode
} from '@modelcontextprotocol/server';
import { toNodeHandler, localhostHostValidation, localhostOriginValidation } from '@modelcontextprotocol/node';
import { createServer } from './servidor.mjs';
const PORTA = Number(process.env.PORTA ?? 3334);
const URL_MCP = new URL(`http://127.0.0.1:${PORTA}/mcp`);
const TOKENS = {
[process.env.TOKEN_LEITURA ?? '']: { clientId: 'leitor', scopes: ['turmas:ler'] },
[process.env.TOKEN_RESERVA ?? '']: { clientId: 'atendente', scopes: ['turmas:ler', 'turmas:reservar'] }
};
if (!process.env.TOKEN_LEITURA || !process.env.TOKEN_RESERVA) {
console.error('defina TOKEN_LEITURA e TOKEN_RESERVA no ambiente'); process.exit(2);
}
const igual = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
const verifier = {
async verifyAccessToken(token) {
const chave = Object.keys(TOKENS).find(k => k && igual(k, token));
if (!chave) throw new OAuthError(OAuthErrorCode.InvalidToken, 'token desconhecido');
return { token, ...TOKENS[chave], expiresAt: Math.floor(Date.now() / 1000) + 3600, resource: URL_MCP };
}
};
const portao = requireBearerAuth({
verifier,
requiredScopes: ['turmas:ler'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(URL_MCP),
expectedResource: URL_MCP
});
// Documento RFC 9728 que o 401 aponta: diz ao cliente qual servidor de autorização usar.
const metadados = {
oauthMetadata: {
issuer: 'https://auth.exemplo.com',
authorization_endpoint: 'https://auth.exemplo.com/authorize',
token_endpoint: 'https://auth.exemplo.com/token',
response_types_supported: ['code'],
authorization_response_iss_parameter_supported: true
},
resourceServerUrl: URL_MCP,
scopesSupported: ['turmas:ler', 'turmas:reservar']
};
const mcp = createMcpHandler(createServer);
const comPortao = {
async fetch(request) {
const doc = oauthMetadataResponse(request, metadados);
if (doc) return doc;
const auth = await portao(request);
if (auth instanceof Response) return auth; // 401 ou 403 pronto
return mcp.fetch(request, { authInfo: auth });
}
};
const node = toNodeHandler(comPortao);
const hostOk = localhostHostValidation();
const origemOk = localhostOriginValidation();
const http = criarHttp((req, res) => {
if (!hostOk(req, res) || !origemOk(req, res)) return;
void node(req, res);
});
http.listen(PORTA, '127.0.0.1', () => console.error(`turmas (com token): ${URL_MCP}`));
process.on('SIGTERM', async () => { await mcp.close(); http.close(); process.exit(0); });annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
// Com token (http-auth.mjs), exige o escopo turmas:reservar; sem token (stdio), não se aplica.
scopeChallenge: requireScopes('turmas:reservar')O bloco gera dois tokens aleatórios só para esta rodada e chama o servidor três vezes.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
export TOKEN_LEITURA=$(openssl rand -hex 16) TOKEN_RESERVA=$(openssl rand -hex 16)
node http-auth.mjs & PID=$!; sleep 1
echo "## sem token"
curl -s -o /dev/null -D - -X POST http://127.0.0.1:3334/mcp \
-H 'Content-Type: application/json' -d '{}' | grep -i -e '^HTTP' -e '^www-authenticate'
echo "## metadados do recurso (RFC 9728)"
curl -s http://127.0.0.1:3334/.well-known/oauth-protected-resource/mcp; echo
echo "## token de leitura"; TOKEN=$TOKEN_LEITURA node teste-auth.mjs
echo "## token de reserva"; TOKEN=$TOKEN_RESERVA node teste-auth.mjs
kill $PID; wait $PID 2>/dev/null// teste-auth.mjs — chama o servidor com token (http-auth.mjs já rodando) e mostra o que cada token pode.
// Uso: TOKEN=<token> node teste-auth.mjs
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const URL_MCP = new URL(`http://127.0.0.1:${process.env.PORTA ?? 3334}/mcp`);
const cliente = new Client({ name: 'teste-auth', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' }, capabilities: { elicitation: { form: {} } } });
cliente.setRequestHandler('elicitation/create', async () => ({ action: 'accept', content: { confirmar: true } }));
await cliente.connect(new StreamableHTTPClientTransport(URL_MCP,
{ requestInit: { headers: { Authorization: `Bearer ${process.env.TOKEN}` } } }));
const busca = await cliente.callTool({ name: 'buscar_turmas', arguments: { termo: 'html' } });
console.log(`buscar_turmas: ${busca.isError ? 'erro' : 'ok'}`);
try {
const r = await cliente.callTool({ name: 'reservar_vaga', arguments: { turma_id: 'html-01', email: 'aluno@exemplo.com' } });
console.log(`reservar_vaga: ${r.content[0].text}`);
} catch (e) {
console.log(`reservar_vaga: recusado (${String(e.message).slice(0, 90)})`);
}
await cliente.close();turmas (com token): http://127.0.0.1:3334/mcp
## sem token
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer error="invalid_token", error_description="Missing Authorization header", scope="turmas:ler", resource_metadata="http://127.0.0.1:3334/.well-known/oauth-protected-resource/mcp"
## metadados do recurso (RFC 9728)
{"resource":"http://127.0.0.1:3334/mcp","authorization_servers":["https://auth.exemplo.com"],"scopes_supported":["turmas:ler","turmas:reservar"]}
## token de leitura
buscar_turmas: ok
reservar_vaga: recusado (Insufficient scope: required "turmas:reservar")
## token de reserva
buscar_turmas: ok
reservar_vaga: reservado: html-01 para aluno@exemplo.com (restam 2)O token de leitura passa no portão do servidor (escopo turmas:ler) mas não reserva: falta turmas:reservar. É o escopo mínimo funcionando por ferramenta.
Token, chave de assinatura e segredo de cliente vêm de variável de ambiente ou cofre. O kit recusa subir sem TOKEN_LEITURA e TOKEN_RESERVA.
Arquivo .env no .gitignore antes do primeiro commit. Antes de publicar, procure: git grep -n -i "token\|secret" -- ':!*.md'.
expectedResource faz o servidor aceitar só token emitido para ele. Sem isso, um token de outro serviço do mesmo IdP passaria.
Um escopo por tipo de ação. Ler e reservar separados; o agente que só consulta recebe só leitura.
O portão do kit e o do seu serviço.
Rode o bloco tokens.
Pronto quando: HTTP/1.1 401 Unauthorized sem token; com o de leitura, reservar_vaga: recusado (Insufficient scope: required "turmas:reservar"); com o de reserva, reservar_vaga: reservado.
Rode node http-auth.mjs; echo "saída: $?" num terminal sem as variáveis.
Pronto quando: defina TOKEN_LEITURA e TOKEN_RESERVA no ambiente e saída: 2.
Liste no mapa-mcp.md um escopo por ferramenta com consequência e ponha scopeChallenge: requireScopes(...) nelas.
Pronto quando: grep -c "scopeChallenge:" servidor.mjs → igual ao número de ferramentas com consequência.
Pergunta 1 de 4
No OAuth do MCP, qual é o papel do seu servidor MCP?competência
Pergunta 2 de 4
O que o 401 do servidor traz para o cliente começar o login?competência
resource_metadata aponta o documento que diz qual servidor de autorização usar e quais escopos existem.resource_metadata aponta o documento que diz qual servidor de autorização usar e quais escopos existem.Rever: O servidor com portãoPergunta 3 de 4
A validação do parâmetro iss (RFC 9207) acontece onde?
Pergunta 4 de 4
Um token com só turmas:ler tenta reservar_vaga. Resposta?
Escolha uma alternativa. A explicação aparece depois da sua tentativa.