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

Nesta aula você vai

  1. 1Bateria com node --test: era nova, era antiga, esquema, MRTR e stdio
  2. 2claude mcp add e codex mcp add, e o que cada um grava
  3. 3server.json e mcp-publisher validate, sem publicar
  4. 4O mapa do curso e o plano com datas

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.

01Do kit ao agente que você usa

Clique em cada etapa.

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

02A bateria automática

O cliente oficial fala com o handler no mesmo processo; o último teste sobe o processo stdio de verdade.

Terminal · npm test
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npm test 2>&1 | grep -E "^(✔|✖|ℹ (tests|pass|fail))"; echo "saída: ${PIPESTATUS[0]}"
Arquivo · kit/teste.mjs
// 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();
});
Exemplo do repositório
✔ 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.

03Ligue ao Claude Code e ao Codex

Comandos locais: não chamam modelo. Rode na pasta do kit (ou na do seu servidor).

Terminal · Claude Code
cd ~/projetos/formacao-webmcp-mcp-v72/kit
claude mcp add -s project turmas -- node "$PWD/stdio.mjs"
claude mcp get turmas
Terminal · Codex
cd ~/projetos/formacao-webmcp-mcp-v72/kit
codex mcp add turmas -- node "$PWD/stdio.mjs"
codex mcp list
Exemplo do repositório
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).

04O agente vai reservar uma vaga

Simulação: você está no Claude Code com o servidor de turmas ligado. Escolha a resposta.

AgenteClaude Code com o servidor turmas

Escolha a sua resposta. O comentário aparece logo depois e a conversa segue conforme a escolha.

    05Descreva o servidor para o Registry (sem publicar)

    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.

    Arquivo · kit/server.json
    {
      "$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"
        }
      ]
    }
    Terminal · validar
    cd ~/projetos/formacao-webmcp-mcp-v72/kit
    mcp-publisher validate      # lê server.json da pasta; não publica nada
    echo "saída: $?"
    Exemplo do repositório
    Validating against https://registry.modelcontextprotocol.io...
    ✅ server.json is valid
    saída: 0

    06Prática

    A bateria, os dois agentes e o seu servidor.

    E21 · Bateria verde

    5 min

    Rode npm test na pasta do kit.

    Pronto quando: npm test → ℹ pass 7, ℹ fail 0 e saída: 0.

    E22 · No Claude Code

    10 min

    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).

    E23 · No Codex

    5 min

    Rode o bloco do Codex.

    Pronto quando: codex mcp list → linha turmas com enabled.

    E24 · server.json do seu serviço

    10 min

    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.

    07O mapa do curso

    Quatro entregas, oito aulas. Clique num exercício para ver o "pronto quando".

    Aulas 1–2Servidor

    stdio + Inspector

    3 exercícios

    Servidor de turmas respondendo por stdio.

    Ver a aula
    Aulas 3–4Ferramentas

    esquema + HTTP

    3 exercícios

    Ferramentas bem descritas, servidas sem estado.

    Ver a aula
    Aulas 5–6Freios

    MRTR + token

    3 exercícios

    Confirmação no meio e escopo mínimo.

    Ver a aula
    Aulas 7–8Entrega

    Apps + testes

    4 exercícios

    Bateria verde e servidor no seu agente.

    Ver a aula
    Ao final do cursoUm servidor MCP na revisão 2026-07-28, testado e ligado ao seu agente.
    24 exercícios
    Clique num exercício para ver o que fazer e o "pronto quando".
    1. 1/4

      Servidor

      Separe as peças e suba o primeiro servidor com versões fixas.

    2. 2/4

      Ferramentas

      Desenho que o modelo entende e o mesmo servidor por Streamable HTTP.

    3. 3/4

      Freios

      Consequência pede sim; remoto pede token com o escopo certo.

    4. 4/4

      Entrega

      Interface opcional, testes automáticos, Claude Code, Codex e Registry.

    Ordem sugerida: cada par de aulas usa o kit da anterior.

    08Gere o seu plano com datas

    Escolha a data de início e quanto tempo você tem por semana.

    09Teste da aula

    Pergunta 1 de 4

    Por que a bateria usa handler.fetch e não InMemoryTransport para testar a revisão nova?competência

    10Em três frases

    Testesnpm test antes de cada mudança: era nova, era antiga, esquema, MRTR, stdio.
    Agentesclaude mcp add e codex mcp add com o mesmo comando de stdio.
    Registryserver.json validado; publicar é opcional e está em preview.

    Para ir além

    Aula anteriorVoltar ao início do curso