E21 · Green test suite
5 minRun npm test in the kit folder.
Done when: npm test → ℹ pass 7, ℹ fail 0 and saída: 0.
Lesson 8 · 20 min
A server is ready when your agent uses it and a test checks it on every change. In this lesson you run the kit's automatic test suite, connect the server to Claude Code and to Codex from your subscription, and prepare the server.json for the official Registry, which is still in preview. It closes with the map and your plan.
Start the lesson
Goal: connect the server to Claude Code and Codex, run the automatic test suite, and prepare the server.json for the Registry, which is in preview.
Click each step.
The official client talks to the handler in the same process; the last test starts the real stdio process.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npm test 2>&1 | grep -E "^(✔|✖|ℹ (tests|pass|fail))"; echo "saída: ${PIPESTATUS[0]}"// teste.mjs — automatic test suite for the turmas server (node --test teste.mjs).
// Runs with no port, no model, and no cost: the official client talks to the handler in the same process.
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
The timings change on every run; what matters is ℹ fail 0. The test for the new revision uses handler.fetch: the in-memory pair (InMemoryTransport) only connects the 2025 era.
Local commands: they don't call a model. Run them in the kit folder (or in your server's folder).
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
Run claude once in that folder and approve the turmas server. After that, claude mcp list should show the server's line ending in ✔ Connected (format seen on other servers; this approval was not captured).
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
The command wrote a [mcp_servers.turmas] block to Codex's config.toml, with command = "node" and the path to stdio.mjs. The capture was made with a separate configuration folder (CODEX_HOME), so the author's own configuration stayed untouched.
Simulation: you're in Claude Code with the turmas server connected. Pick your reply.
Choose your answer. The feedback appears right after, and the conversation continues based on your choice.
Registry in preview as of 10/19/2026. You need mcp-publisher (releases of the modelcontextprotocol/registry repository). validate queries registry.modelcontextprotocol.io and publishes nothing.
{
"$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 # reads server.json from the folder; publishes nothing 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: 1The Registry limits description to 100 characters.
The test suite, the two agents, and your server.
Run npm test in the kit folder.
Done when: npm test → ℹ pass 7, ℹ fail 0 and saída: 0.
Run the Claude Code block, open claude in the folder, approve the server, and ask "tem turma de html com vaga?". Use your subscription's model sparingly: one question is enough.
Done when: claude mcp list | grep turmas → a line ending in ✔ Connected (the model's answer text is not the criterion).
Run the Codex block.
Done when: codex mcp list → a turmas line with enabled.
Replace the name, description, repository, and URL in server.json with those of your service. Publishing is optional and requires a verified namespace.
Done when: mcp-publisher validate → ✅ server.json is valid.
Four deliverables, eight lessons. Click an exercise to see its "done when".
Apps + tests
4 exercises
Green test suite and the server in your agent.
View the lessonSeparate the pieces and bring up the first server with pinned versions.
A design the model understands, and the same server over Streamable HTTP.
A consequence asks for sim; remote asks for a token with the right scope.
Optional UI, automated tests, Claude Code, Codex and Registry.
Suggested order: each pair of lessons uses the kit from the previous one.
Pick a start date and how much time you have each week.
Question 1 of 4
Why does the test suite use handler.fetch and not InMemoryTransport to test the new revision?skill
Question 2 of 4
After claude mcp add -s project, claude mcp get shows Pending approval. What's missing?skill
Question 3 of 4
What does mcp-publisher validate do?
Question 4 of 4
As of 10/19/2026, how should the official Registry be treated?
Pick an option. The explanation appears after you try.