Servidor MCP de tu servicio|INEMA.CLUB-PRO

En esta lección vas a

  1. 1Instalar el kit con npm ci y comprobar las versiones
  2. 2La fábrica createServer y el serveStdio del SDK v2
  3. 3Llamar a la tool con el Inspector 2.11.0 en modo --cli
  4. 4Por qué todo log va al stderr

Objetivo: levantar un servidor MCP por stdio con el SDK oficial v2 y llamarlo con un client de prueba y con el Inspector, sin ningún host.

01Del npm ci a la primera llamada

Haz clic en cada etapa.

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

02Instala el kit

Una sola vez. Necesitas Node 20 o más nuevo y git. Los bloques de las próximas clases parten de esta carpeta.

Terminal · instalar
git clone https://github.com/inematds/formacao-webmcp-mcp-v72 ~/projetos/formacao-webmcp-mcp-v72
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npm ci                      # instala las versiones del package-lock.json
npm ls --depth=0
Ejemplo del repositorio
Cloning into '~/projetos/formacao-webmcp-mcp-v72'...

added 19 packages, and audited 20 packages in 3s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
kit-servidor-mcp@1.0.0 ~/projetos/formacao-webmcp-mcp-v72/kit
├── @modelcontextprotocol/client@2.3.1
├── @modelcontextprotocol/ext-apps@2.0.3
├── @modelcontextprotocol/node@2.1.1
├── @modelcontextprotocol/server@2.3.1
└── zod@4.6.7

En la captura, la carpeta personal fue reemplazada por ~; en tu terminal aparece la ruta completa.

03El servidor de grupos

Lee antes de ejecutar. Las dos tools, el resource y el prompt están en la misma fábrica; el stdio es solo la puerta de entrada.

Archivo · kit/servidor.mjs
// servidor.mjs — el servidor MCP de ejemplo del curso (servicio ficticio de turmas).
// SDK oficial v2 (@modelcontextprotocol/server 2.3.1, del 05/10/2026), que sirve la
// revisión 2026-07-28 del MCP y, con el mismo código, a clientes de la revisión 2025-11-25.
// No llama a ningún modelo ni servicio de pago: los datos quedan en la memoria del proceso.
import { McpServer, acceptedContent, inputRequired, inputResponse, requireScopes } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

export const TURMAS = [
  { id: 'html-01', titulo: 'HTML semântico para agentes', vagas: 3 },
  { id: 'mcp-01', titulo: 'Servidor MCP em uma tarde', vagas: 1 },
  { id: 'mcp-02', titulo: 'MCP com autorização', vagas: 0 }
];

const Turma = z.object({ id: z.string(), titulo: z.string(), vagas: z.int() });
const Confirma = z.object({ confirmar: z.boolean().meta({ title: 'Confirmar a reserva' }) });

export function createServer() {
  const server = new McpServer({ name: 'turmas', version: '1.0.0' });

  // 1) Tool de solo lectura: el cliente puede aprobarla sin preguntar.
  server.registerTool(
    'buscar_turmas',
    {
      title: 'Buscar turmas',
      description: 'Lista turmas cujo título contém o termo. Só leitura: não reserva nada.',
      inputSchema: z.object({
        termo: z.string().min(2).describe('Parte do título, ex.: "mcp"'),
        limite: z.int().min(1).max(20).optional().describe('Máximo de turmas (padrão 5)')
      }),
      outputSchema: z.object({ turmas: z.array(Turma) }),
      annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
    },
    async ({ termo, limite }) => {
      const t = termo.toLowerCase();
      const turmas = TURMAS.filter(x => x.titulo.toLowerCase().includes(t)).slice(0, limite ?? 5);
      return {
        content: [{ type: 'text', text: turmas.map(x => `${x.id} · ${x.titulo} · ${x.vagas} vagas`).join('\n') || 'nenhuma turma' }],
        structuredContent: { turmas }
      };
    }
  );

  // 2) Tool con consecuencia: pide confirmación al usuario a mitad de la llamada (MRTR).
  server.registerTool(
    'reservar_vaga',
    {
      title: 'Reservar vaga',
      description: 'Reserva uma vaga na turma para o e-mail informado. Tem consequência: pede confirmação antes.',
      inputSchema: z.object({
        turma_id: z.string().describe('Id da turma, ex.: "mcp-01"'),
        email: z.email().describe('E-mail de quem vai fazer a turma')
      }),
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
      // Con token (http-auth.mjs), exige el scope turmas:reservar; sin token (stdio), no se aplica.
      scopeChallenge: requireScopes('turmas:reservar')
    },
    async ({ turma_id, email }, ctx) => {
      const turma = TURMAS.find(x => x.id === turma_id);
      if (!turma) return { content: [{ type: 'text', text: `turma ${turma_id} não existe` }], isError: true };
      if (turma.vagas < 1) return { content: [{ type: 'text', text: `turma ${turma_id} sem vagas` }], isError: true };

      const resposta = inputResponse(ctx.mcpReq.inputResponses, 'confirmar');
      if (resposta.kind === 'elicit' && resposta.action !== 'accept') {
        return { content: [{ type: 'text', text: 'reserva cancelada pelo usuário' }], isError: true };
      }
      const ok = acceptedContent(ctx.mcpReq.inputResponses, 'confirmar', Confirma);
      if (ok?.confirmar !== true) {
        return inputRequired({
          inputRequests: {
            confirmar: inputRequired.elicit({ message: `Reservar 1 vaga em "${turma.titulo}" para ${email}?`, requestedSchema: Confirma })
          }
        });
      }
      turma.vagas -= 1;
      return { content: [{ type: 'text', text: `reservado: ${turma_id} para ${email} (restam ${turma.vagas})` }] };
    }
  );

  // 3) Resource: dato que el cliente lee cuando quiera (no es una acción).
  server.registerResource(
    'catalogo',
    'turmas://catalogo',
    { title: 'Catálogo de turmas', description: 'Todas as turmas em JSON', mimeType: 'application/json' },
    async uri => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(TURMAS) }] })
  );

  // 4) Prompt: plantilla de pedido que el usuario elige en el cliente.
  server.registerPrompt(
    'convite_turma',
    {
      title: 'Convite para a turma',
      description: 'Monta o pedido de um convite curto para uma turma',
      argsSchema: z.object({ turma_id: z.string() })
    },
    async ({ turma_id }) => ({
      messages: [{ role: 'user', content: { type: 'text', text: `Escreva um convite de 3 linhas para a turma ${turma_id}, usando o recurso turmas://catalogo.` } }]
    })
  );

  return server;
}
Archivo · kit/stdio.mjs
// stdio.mjs — sirve el servidor de turmas por stdio (el cliente inicia este proceso).
// stdout es el canal del protocolo: todo log va a stderr (console.error).
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { createServer } from './servidor.mjs';

serveStdio(createServer);
console.error('turmas: servidor MCP no stdio');

04Llama con el cliente de prueba

La prueba hace el papel de Claude Code: inicia node stdio.mjs, negocia la revisión y llama a buscar_turmas.

Terminal · prueba stdio
cd ~/projetos/formacao-webmcp-mcp-v72/kit
node teste-stdio.mjs; echo "saída: $?"
Archivo · kit/teste-stdio.mjs
// Uso: node teste-stdio.mjs [arquivo do servidor, padrão stdio.mjs]
// teste-stdio.mjs — hace el papel del cliente (Claude Code, Codex…): inicia "node stdio.mjs",
// negocia la revisión del protocolo y llama a una tool por el stdin/stdout del proceso.
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const cliente = new Client({ name: 'teste-stdio', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await cliente.connect(new StdioClientTransport({ command: 'node', args: [process.argv[2] ?? 'stdio.mjs'] }));
const { tools } = await cliente.listTools();
const r = await cliente.callTool({ name: 'buscar_turmas', arguments: { termo: 'mcp' } });
console.log(`era: ${cliente.getProtocolEra()} · ferramentas: ${tools.map(t => t.name).join(', ')}`);
console.log(`buscar_turmas → ${r.structuredContent.turmas.length} turmas`);
await cliente.close();
Terminal · error común (sin npm ci)
cd "$(mktemp -d)" && cp ~/projetos/formacao-webmcp-mcp-v72/kit/*.mjs ~/projetos/formacao-webmcp-mcp-v72/kit/package.json .
node teste-stdio.mjs 2>&1 | grep -m1 -o "ERR_MODULE_NOT_FOUND\|Cannot find package '[^']*'"; echo "saída: ${PIPESTATUS[0]}"
Ejemplo del repositorio
turmas: servidor MCP no stdio
era: modern · ferramentas: buscar_turmas, reservar_vaga
buscar_turmas → 2 turmas
saída: 0

La primera línea vino del stderr del servidor; el resto, del cliente.

05Llama con el MCP Inspector, sin abrir el navegador

El Inspector 2.11.0 tiene modo línea de comandos. La versión está fijada en npx para que el resultado sea el mismo de la captura.

Terminal · Inspector --cli
cd ~/projetos/formacao-webmcp-mcp-v72/kit
npx -y @modelcontextprotocol/inspector@2.11.0 --cli node stdio.mjs \
  --method tools/call --tool-name buscar_turmas --tool-arg termo=html
echo "saída: $?"
Ejemplo del repositorio
turmas: servidor MCP no stdio
{
  "content": [
    {
      "type": "text",
      "text": "html-01 · HTML semântico para agentes · 3 vagas"
    }
  ],
  "structuredContent": {
    "turmas": [
      {
        "id": "html-01",
        "titulo": "HTML semântico para agentes",
        "vagas": 3
      }
    ]
  }
}
saída: 0

Sin --cli, el mismo comando abre la interfaz web del Inspector, donde te conectas y pruebas las tools haciendo clic.

06stdout es del protocolo

En stdio, cada línea de stdout se lee como un mensaje JSON-RPC. Por eso el kit registra los logs con console.error (stderr). La documentación del SDK avisa que un console.log estropea el canal y el host puede cerrar la conexión.

En nuestra prueba del 19/10/2026, el cliente del SDK 2.3.1 y el Inspector 2.11.0 se saltaron una línea sucia y siguieron. No cuentes con eso: otro host puede no perdonarlo. Los logs, siempre en stderr.

¿Prefieres Python? El paquete del SDK oficial mcp 2.3.0 (PyPI, descargado el 19/10/2026) contiene server/discover e InputRequiredResult en el código; revisa las notas de versión del python-sdk antes de usarlo. Este curso sigue en Node; los conceptos valen igual.

07Práctica

Ensayo en el kit y un cambio tuyo.

E3 · Kit instalado

5 min

Ejecuta el bloque instalar.

Listo cuando: npm ls --depth=0 → muestra @modelcontextprotocol/server@2.3.1 y @modelcontextprotocol/client@2.3.1.

E4 · Primera llamada

5 min

Ejecuta la prueba stdio y el Inspector --cli.

Listo cuando: node teste-stdio.mjs → era: modern · ferramentas: buscar_turmas, reservar_vaga y saída: 0; el Inspector devuelve un JSON con "structuredContent".

E5 · Un grupo tuyo

10 min

Agrega un grupo en TURMAS dentro de servidor.mjs con la palabra mcp en el título y ejecuta la prueba de nuevo.

Listo cuando: node teste-stdio.mjs → buscar_turmas → 3 turmas.

08Prueba de la clase

Pregunta 1 de 4

En el transporte stdio, ¿quién inicia el proceso del servidor?competencia

09En tres frases

Versiónnpm ci con el lock del kit: SDK v2 2.3.1.
ServerUna fábrica createServer; serveStdio es la puerta.
PruebaCliente del SDK e Inspector --cli, sin host y sin costo.

Para ir más allá

Clase anteriorSiguiente clase