Servidor MCP do seu serviço|INEMA.CLUB-PRO

Nesta aula você vai

  1. 1Quem emite (servidor de autorização) e quem confere (o seu servidor MCP)
  2. 2401 com WWW-Authenticate e o documento RFC 9728
  3. 3Escopo do servidor inteiro e escopo por ferramenta (403 insufficient_scope)
  4. 4iss (RFC 9207) e credencial presa ao emissor: o que é do cliente

Objetivo: proteger o servidor HTTP com token: 401 com metadados, escopo mínimo por ferramenta e segredo fora do repositório.

01Do 401 ao token com o escopo certo

Clique em cada etapa.

Clique em cada bloco do diagrama para ver o que ele faz.

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.

02O servidor com portão

Ensaio: dois tokens vêm de variáveis de ambiente. Em produção, troque o verificador pela checagem do JWT do seu IdP.

Arquivo · kit/http-auth.mjs
// 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); });
Trecho · escopo por ferramenta (kit/servidor.mjs)
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')

03Sem token, token de leitura, token de reserva

O bloco gera dois tokens aleatórios só para esta rodada e chama o servidor três vezes.

Terminal · tokens
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
Arquivo · kit/teste-auth.mjs
// 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();
Exemplo do repositório
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.

04Segredo fora do repositório

1. Ambiente, não código

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.

05Prática

O portão do kit e o do seu serviço.

E15 · Três chamadas

5 min

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.

E16 · Sem variável, sem servidor

2 min

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.

E17 · Escopos do seu serviço

15 min

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.

06Teste da aula

Pergunta 1 de 4

No OAuth do MCP, qual é o papel do seu servidor MCP?competência

07Em três frases

PapelO servidor MCP confere token; quem emite é o IdP.
EscopoMínimo, por ferramenta, com 403 quando falta.
SegredoNo ambiente, nunca no repositório; audiência conferida.

Para ir além

Aula anteriorPróxima aula