E21 · Batería en verde
5 minEjecuta npm test en la carpeta del kit.
Listo cuando: npm test → ℹ pass 7, ℹ fail 0 y saída: 0.
Clase 8 · 20 min
Un server listo es un server que tu agente usa y que una prueba verifica en cada cambio. En esta clase ejecutas la batería automática del kit, conectas el server a Claude Code y a Codex de tu suscripción y preparas el server.json del Registry oficial, que todavía está en preview. Cierra con el mapa y tu plan.
Comenzar la lección
Objetivo: conectar el server a Claude Code y a Codex, ejecutar la batería automática y preparar el server.json del Registry, que está en preview.
Haz clic en cada etapa.
El cliente oficial habla con el handler en el mismo proceso; la última prueba levanta el proceso stdio de verdad.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npm test 2>&1 | grep -E "^(✔|✖|ℹ (tests|pass|fail))"; echo "saída: ${PIPESTATUS[0]}"// teste.mjs — batería automática del servidor de turmas (node --test teste.mjs).
// Corre sin puerto, sin modelo y sin costo: el cliente oficial habla con el handler en el mismo proceso.
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
Los tiempos cambian en cada ronda; lo que importa es ℹ fail 0. La prueba de la revisión nueva usa handler.fetch: el par en memoria (InMemoryTransport) solo conecta la era 2025.
Comandos locales: no llaman a ningún modelo. Ejecútalos en la carpeta del kit (o en la de tu 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
Ejecuta claude una vez en esa carpeta y aprueba el server turmas. Después, claude mcp list debería mostrar la línea del server terminando en ✔ Connected (formato visto en otros servers; esta aprobación no fue 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
El comando grabó en el config.toml de Codex un bloque [mcp_servers.turmas] con command = "node" y la ruta del stdio.mjs. La captura se hizo con una carpeta de configuración separada (CODEX_HOME), para no tocar la configuración del autor.
Simulación: estás en Claude Code con el servidor de turmas conectado. Elige la respuesta.
Elige tu respuesta. El comentario aparece enseguida y la conversación sigue según tu elección.
Registry en preview el 19/10/2026. Necesita mcp-publisher (releases del repositorio modelcontextprotocol/registry). validate consulta registry.modelcontextprotocol.io y no 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 # lee server.json de la carpeta; no 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: 1El Registry limita description a 100 caracteres.
La batería, los dos agentes y tu servidor.
Ejecuta npm test en la carpeta del kit.
Listo cuando: npm test → ℹ pass 7, ℹ fail 0 y saída: 0.
Ejecuta el bloque de Claude Code, abre claude en la carpeta, aprueba el servidor y pregunta "tem turma de html com vaga?". Usa el modelo de tu suscripción con moderación: una pregunta basta.
Listo cuando: claude mcp list | grep turmas → línea que termina en ✔ Connected (el texto de la respuesta del modelo no es criterio).
Ejecuta el bloque de Codex.
Listo cuando: codex mcp list → línea turmas con enabled.
Cambia nombre, descripción, repositorio y URL en el server.json por los de tu servicio. Publicar es opcional y exige un namespace verificado.
Listo cuando: mcp-publisher validate → ✅ server.json is valid.
Cuatro entregas, ocho clases. Haz clic en un ejercicio para ver el "listo cuando".
MRTR + token
3 ejercicios
Confirmación a mitad de la llamada y scope mínimo.
Ver la claseSepara las piezas y levanta el primer servidor con versiones fijas.
Un diseño que el modelo entiende y el mismo servidor por Streamable HTTP.
La consecuencia pide sim; lo remoto pide token con el scope correcto.
Interfaz opcional, pruebas automáticas, Claude Code, Codex y Registry.
Orden sugerido: cada par de clases usa el kit de la anterior.
Elige la fecha de inicio y cuánto tiempo tienes por semana.
Pregunta 1 de 4
¿Por qué la batería usa handler.fetch y no InMemoryTransport para probar la revisión nueva?competencia
Pregunta 2 de 4
Después de claude mcp add -s project, claude mcp get muestra Pending approval. ¿Qué falta?competencia
Pregunta 3 de 4
¿Qué hace mcp-publisher validate?
Pregunta 4 de 4
El 19/10/2026, ¿cómo se debe tratar el Registry oficial?
Elige una alternativa. La explicación aparece después de tu intento.