Servidor MCP de tu servicio|INEMA.CLUB-PRO

En esta lección vas a

  1. 1Batería con node --test: era nueva, era antigua, esquema, MRTR y stdio
  2. 2claude mcp add y codex mcp add, y qué graba cada uno
  3. 3server.json y mcp-publisher validate, sin publicar
  4. 4El mapa del curso y el plan con fechas

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.

01Del kit al agente que usas

Haz clic en cada etapa.

Haz clic en cada bloque del diagrama para ver qué hace.

02La batería automática

El cliente oficial habla con el handler en el mismo proceso; la última prueba levanta el proceso stdio de verdad.

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

03Conéctalo a Claude Code y a Codex

Comandos locales: no llaman a ningún modelo. Ejecútalos en la carpeta del kit (o en la de tu 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
Ejemplo del repositorio
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).

04El agente va a reservar una plaza

Simulación: estás en Claude Code con el servidor de turmas conectado. Elige la respuesta.

AgenteClaude Code con el servidor turmas

Elige tu respuesta. El comentario aparece enseguida y la conversación sigue según tu elección.

    05Describe el servidor para el Registry (sin publicar)

    Registry en preview el 19/10/2026. Necesita mcp-publisher (releases del repositorio modelcontextprotocol/registry). validate consulta registry.modelcontextprotocol.io y no publica nada.

    Archivo · 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      # lee server.json de la carpeta; no publica nada
    echo "saída: $?"
    Ejemplo del repositorio
    Validating against https://registry.modelcontextprotocol.io...
    ✅ server.json is valid
    saída: 0

    06Práctica

    La batería, los dos agentes y tu servidor.

    E21 · Batería en verde

    5 min

    Ejecuta npm test en la carpeta del kit.

    Listo cuando: npm test → ℹ pass 7, ℹ fail 0 y saída: 0.

    E22 · En Claude Code

    10 min

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

    E23 · En Codex

    5 min

    Ejecuta el bloque de Codex.

    Listo cuando: codex mcp list → línea turmas con enabled.

    E24 · server.json de tu servicio

    10 min

    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.

    07El mapa del curso

    Cuatro entregas, ocho clases. Haz clic en un ejercicio para ver el "listo cuando".

    Clases 1–2Server

    stdio + Inspector

    3 ejercicios

    Servidor de turmas respondiendo por stdio.

    Ver la clase
    Clases 3–4Tools

    esquema + HTTP

    3 ejercicios

    Tools bien descritas, servidas sin estado.

    Ver la clase
    Clases 5–6Frenos

    MRTR + token

    3 ejercicios

    Confirmación a mitad de la llamada y scope mínimo.

    Ver la clase
    Clases 7–8Entrega

    Apps + pruebas

    4 ejercicios

    Batería en verde y servidor en tu agente.

    Ver la clase
    Al final del cursoUn servidor MCP en la revisión 2026-07-28, probado y conectado a tu agente.
    24 ejercicios
    Haz clic en un ejercicio para ver qué hacer y el "listo cuando".
    1. 1/4

      Server

      Separa las piezas y levanta el primer servidor con versiones fijas.

    2. 2/4

      Tools

      Un diseño que el modelo entiende y el mismo servidor por Streamable HTTP.

    3. 3/4

      Frenos

      La consecuencia pide sim; lo remoto pide token con el scope correcto.

    4. 4/4

      Entrega

      Interfaz opcional, pruebas automáticas, Claude Code, Codex y Registry.

    Orden sugerido: cada par de clases usa el kit de la anterior.

    08Genera tu plan con fechas

    Elige la fecha de inicio y cuánto tiempo tienes por semana.

    09Prueba de la clase

    Pregunta 1 de 4

    ¿Por qué la batería usa handler.fetch y no InMemoryTransport para probar la revisión nueva?competencia

    10En tres frases

    Pruebasnpm test antes de cada cambio: era nueva, era antigua, esquema, MRTR, stdio.
    Agentesclaude mcp add y codex mcp add con el mismo comando de stdio.
    Registryserver.json validado; publicar es opcional y está en preview.

    Para ir más allá

    Clase anteriorVolver al inicio del curso