An MCP Server for Your Service|INEMA.CLUB-PRO

Lesson 8 · 20 min

Connect to Claude Code/Codex, automatic tests and Registry (preview)

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
Notebook with a blurred terminal and test lines, a connector being plugged into a mini server and a clipboard.

In this lesson you will

  1. 1Test suite with node --test: new era, old era, schema, MRTR and stdio
  2. 2claude mcp add and codex mcp add, and what each one writes
  3. 3server.json and mcp-publisher validate, without publishing
  4. 4The course map and the plan with dates

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.

01From the kit to the agent you use

Click each step.

Click each block in the diagram to see what it does.

02The automatic test suite

The official client talks to the handler in the same process; the last test starts the real stdio process.

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

03Connect it to Claude Code and Codex

Local commands: they don't call a model. Run them in the kit folder (or in your server's folder).

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
Example from the repository
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).

04The agent is going to reserve a seat

Simulation: you're in Claude Code with the turmas server connected. Pick your reply.

AgentClaude Code with the turmas server

Choose your answer. The feedback appears right after, and the conversation continues based on your choice.

    05Describe the server for the Registry (without publishing)

    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.

    File · 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 · validate
    cd ~/projetos/formacao-webmcp-mcp-v72/kit
    mcp-publisher validate      # reads server.json from the folder; publishes nothing
    echo "saída: $?"
    Example from the repository
    Validating against https://registry.modelcontextprotocol.io...
    ✅ server.json is valid
    saída: 0

    06Practice

    The test suite, the two agents, and your server.

    E21 · Green test suite

    5 min

    Run npm test in the kit folder.

    Done when: npm test → ℹ pass 7, ℹ fail 0 and saída: 0.

    E22 · In Claude Code

    10 min

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

    E23 · In Codex

    5 min

    Run the Codex block.

    Done when: codex mcp list → a turmas line with enabled.

    E24 · server.json for your service

    10 min

    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.

    07The course map

    Four deliverables, eight lessons. Click an exercise to see its "done when".

    Lessons 1–2Server

    stdio + Inspector

    3 exercises

    Class server responding over stdio.

    View the lesson
    Lessons 3–4Tools

    schema + HTTP

    3 exercises

    Well-described tools, served stateless.

    View the lesson
    Lessons 5–6Brakes

    MRTR + token

    3 exercises

    Mid-call confirmation and minimum scope.

    View the lesson
    Lessons 7–8Delivery

    Apps + tests

    4 exercises

    Green test suite and the server in your agent.

    View the lesson
    By the end of the courseAn MCP server on the 2026-07-28 revision, tested and connected to your agent.
    24 exercises
    Click an exercise to see what to do and the "done when".
    1. 1/4

      Server

      Separate the pieces and bring up the first server with pinned versions.

    2. 2/4

      Tools

      A design the model understands, and the same server over Streamable HTTP.

    3. 3/4

      Brakes

      A consequence asks for sim; remote asks for a token with the right scope.

    4. 4/4

      Delivery

      Optional UI, automated tests, Claude Code, Codex and Registry.

    Suggested order: each pair of lessons uses the kit from the previous one.

    08Build your plan with dates

    Pick a start date and how much time you have each week.

    09Lesson quiz

    Question 1 of 4

    Why does the test suite use handler.fetch and not InMemoryTransport to test the new revision?skill

    10In three sentences

    Testsnpm test before every change: new rehearsal, old rehearsal, schema, MRTR, stdio.
    Agentsclaude mcp add and codex mcp add with the same stdio command.
    Registryserver.json validated; publishing is optional and in preview.

    Going further