E21 · Bateria verde
5 minRode npm test na pasta do kit.
Pronto quando: npm test → ℹ pass 7, ℹ fail 0 e saída: 0.
Aula 8 · 20 min
Servidor pronto é servidor que o seu agente usa e que um teste confere a cada mudança. Nesta aula você roda a bateria automática do kit, liga o servidor ao Claude Code e ao Codex da sua assinatura e prepara o server.json do Registry oficial, que ainda está em preview. Fecha com o mapa e o seu plano.
Começar a aula
Objetivo: ligar o servidor ao Claude Code e ao Codex, rodar a bateria automática e preparar o server.json do Registry, que está em preview.
Clique em cada etapa.
O cliente oficial fala com o handler no mesmo processo; o último teste sobe o processo stdio de verdade.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npm test 2>&1 | grep -E "^(✔|✖|ℹ (tests|pass|fail))"; echo "saída: ${PIPESTATUS[0]}"// teste.mjs — bateria automática do servidor de turmas (node --test teste.mjs).
// Roda sem porta, sem modelo e sem custo: o cliente oficial fala com o handler no mesmo processo.
import { test, after } from 'node:test';
import assert from 'node:assert/strict';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { createServer, TURMAS } from './servidor.mjs';
const handler = createMcpHandler(createServer);
after(() => handler.close());
async function cliente(modo = 'auto', responde = null) {
const c = new Client({ name: 'teste', version: '1.0.0' },
{ versionNegotiation: { mode: modo }, capabilities: { elicitation: { form: {} } } });
if (responde) c.setRequestHandler('elicitation/create', async () => responde);
await c.connect(new StreamableHTTPClientTransport(new URL('http://teste.local/mcp'),
{ fetch: (url, init) => handler.fetch(new Request(url, init)) }));
return c;
}
test('cliente novo negocia a revisão 2026-07-28 (era modern)', async () => {
const c = await cliente('auto');
assert.equal(c.getProtocolEra(), 'modern');
await c.close();
});
test('cliente antigo (initialize, 2025-11-25) ainda é atendido', async () => {
const c = await cliente('legacy');
assert.equal(c.getProtocolEra(), 'legacy');
const { tools } = await c.listTools();
assert.ok(tools.some(t => t.name === 'buscar_turmas'));
await c.close();
});
test('buscar_turmas é só leitura e devolve dado estruturado', async () => {
const c = await cliente();
const { tools } = await c.listTools();
assert.equal(tools.find(t => t.name === 'buscar_turmas').annotations.readOnlyHint, true);
const r = await c.callTool({ name: 'buscar_turmas', arguments: { termo: 'html' } });
assert.deepEqual(r.structuredContent.turmas.map(t => t.id), ['html-01']);
await c.close();
});
test('argumento inválido volta como isError, sem rodar o handler', async () => {
const c = await cliente();
const r = await c.callTool({ name: 'buscar_turmas', arguments: { termo: 'x' } });
assert.equal(r.isError, true);
await c.close();
});
test('reservar_vaga só reserva depois da confirmação (MRTR)', async () => {
const antes = TURMAS.find(t => t.id === 'mcp-01').vagas;
const recusa = await cliente('auto', { action: 'decline' });
const r1 = await recusa.callTool({ name: 'reservar_vaga', arguments: { turma_id: 'mcp-01', email: 'a@exemplo.com' } });
assert.equal(r1.isError, true);
assert.equal(TURMAS.find(t => t.id === 'mcp-01').vagas, antes);
await recusa.close();
const aceita = await cliente('auto', { action: 'accept', content: { confirmar: true } });
const r2 = await aceita.callTool({ name: 'reservar_vaga', arguments: { turma_id: 'mcp-01', email: 'a@exemplo.com' } });
assert.equal(r2.isError, undefined);
assert.equal(TURMAS.find(t => t.id === 'mcp-01').vagas, antes - 1);
await aceita.close();
});
test('turma sem vaga é recusada com isError', async () => {
const c = await cliente('auto', { action: 'accept', content: { confirmar: true } });
const r = await c.callTool({ name: 'reservar_vaga', arguments: { turma_id: 'mcp-02', email: 'a@exemplo.com' } });
assert.equal(r.isError, true);
await c.close();
});
test('o processo stdio sobe e lista as ferramentas', async () => {
const c = new Client({ name: 'teste-stdio', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await c.connect(new StdioClientTransport({ command: process.execPath, args: [new URL('./stdio.mjs', import.meta.url).pathname] }));
const { tools } = await c.listTools();
assert.deepEqual(tools.map(t => t.name).sort(), ['buscar_turmas', 'reservar_vaga']);
await c.close();
});✔ cliente novo negocia a revisão 2026-07-28 (era modern) (43.097669ms) ✔ cliente antigo (initialize, 2025-11-25) ainda é atendido (37.721172ms) ✔ buscar_turmas é só leitura e devolve dado estruturado (20.015635ms) ✔ argumento inválido volta como isError, sem rodar o handler (3.965184ms) ✔ reservar_vaga só reserva depois da confirmação (MRTR) (15.684146ms) ✔ turma sem vaga é recusada com isError (2.665568ms) ✔ o processo stdio sobe e lista as ferramentas (243.920508ms) ℹ tests 7 ℹ pass 7 ℹ fail 0 saída: 0
Os tempos mudam a cada rodada; o que importa é ℹ fail 0. Teste da revisão nova usa handler.fetch: o par em memória (InMemoryTransport) só conecta a era 2025.
Comandos locais: não chamam modelo. Rode na pasta do kit (ou na do seu servidor).
cd ~/projetos/formacao-webmcp-mcp-v72/kit claude mcp add -s project turmas -- node "$PWD/stdio.mjs" claude mcp get turmas
cd ~/projetos/formacao-webmcp-mcp-v72/kit codex mcp add turmas -- node "$PWD/stdio.mjs" codex mcp list
Added stdio MCP server turmas with command: node /home/nmaldaner/projetos/formacao-webmcp-mcp-v72/kit/stdio.mjs to project config File modified: /home/nmaldaner/projetos/formacao-webmcp-mcp-v72/kit/.mcp.json turmas: Scope: Project config (shared via .mcp.json) Status: ⏸ Pending approval (run `claude` to approve) Type: stdio Command: node Args: /home/nmaldaner/projetos/formacao-webmcp-mcp-v72/kit/stdio.mjs Environment: To remove this server, run: claude mcp remove turmas -s project
Rode claude uma vez nessa pasta e aprove o servidor turmas. Depois, claude mcp list deve mostrar a linha do servidor terminando em ✔ Connected (formato visto em outros servidores; esta aprovação não foi capturada).
Added global MCP server 'turmas'. Name Command Args Env Cwd Status Auth turmas node /home/nmaldaner/projetos/formacao-webmcp-mcp-v72/kit/stdio.mjs - - enabled Unsupported
O comando gravou no config.toml do Codex um bloco [mcp_servers.turmas] com command = "node" e o caminho do stdio.mjs. Captura feita com uma pasta de configuração separada (CODEX_HOME), para não mexer na configuração do autor.
Simulação: você está no Claude Code com o servidor de turmas ligado. Escolha a resposta.
Escolha a sua resposta. O comentário aparece logo depois e a conversa segue conforme a escolha.
Registry em preview em 19/10/2026. Precisa do mcp-publisher (releases do repositório modelcontextprotocol/registry). validate consulta registry.modelcontextprotocol.io e não publica nada.
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.seu-usuario/turmas",
"title": "Turmas",
"description": "Busca turmas e reserva vaga com confirmação do usuário.",
"repository": {
"url": "https://github.com/seu-usuario/turmas-mcp",
"source": "github"
},
"version": "1.0.0",
"remotes": [
{
"type": "streamable-http",
"url": "https://turmas.exemplo.com/mcp"
}
]
}cd ~/projetos/formacao-webmcp-mcp-v72/kit mcp-publisher validate # lê server.json da pasta; não publica nada echo "saída: $?"
Validating against https://registry.modelcontextprotocol.io... ✅ server.json is valid saída: 0
Validating against https://registry.modelcontextprotocol.io...
Error: validation failed: server returned status 422: {"title":"Unprocessable Entity","status":422,"detail":"validation failed","errors":[{"message":"expected length <= 100","location":"body.description","value":"Busca turmas e reserva vaga com confirmação do usuário, mostra painel, conversa com o agente e muito mais coisas úteis aqui."}]}
saída: 1O Registry limita description a 100 caracteres.
A bateria, os dois agentes e o seu servidor.
Rode npm test na pasta do kit.
Pronto quando: npm test → ℹ pass 7, ℹ fail 0 e saída: 0.
Rode o bloco do Claude Code, abra claude na pasta, aprove o servidor e peça "tem turma de html com vaga?". Use o modelo da sua assinatura com parcimônia: uma pergunta basta.
Pronto quando: claude mcp list | grep turmas → linha terminando em ✔ Connected (o texto da resposta do modelo não é critério).
Rode o bloco do Codex.
Pronto quando: codex mcp list → linha turmas com enabled.
Troque nome, descrição, repositório e URL no server.json pelos do seu serviço. Publicar é opcional e exige namespace verificado.
Pronto quando: mcp-publisher validate → ✅ server.json is valid.
Quatro entregas, oito aulas. Clique num exercício para ver o "pronto quando".
esquema + HTTP
3 exercícios
Ferramentas bem descritas, servidas sem estado.
Ver a aulaSepare as peças e suba o primeiro servidor com versões fixas.
Desenho que o modelo entende e o mesmo servidor por Streamable HTTP.
Consequência pede sim; remoto pede token com o escopo certo.
Interface opcional, testes automáticos, Claude Code, Codex e Registry.
Ordem sugerida: cada par de aulas usa o kit da anterior.
Escolha a data de início e quanto tempo você tem por semana.
Pergunta 1 de 4
Por que a bateria usa handler.fetch e não InMemoryTransport para testar a revisão nova?competência
Pergunta 2 de 4
Depois de claude mcp add -s project, claude mcp get mostra Pending approval. O que falta?competência
Pergunta 3 de 4
O que o mcp-publisher validate faz?
Pergunta 4 de 4
Em 19/10/2026, como o Registry oficial deve ser tratado?
Escolha uma alternativa. A explicação aparece depois da sua tentativa.