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

Nesta aula você vai

  1. 1Instalar o kit com npm ci e conferir as versões
  2. 2A fábrica createServer e o serveStdio do SDK v2
  3. 3Chamar a ferramenta pelo Inspector 2.11.0 em modo --cli
  4. 4Por que todo log vai para o stderr

Objetivo: subir um servidor MCP por stdio com o SDK oficial v2 e chamá-lo por um cliente de teste e pelo Inspector, sem host nenhum.

01Do npm ci à primeira chamada

Clique em cada etapa.

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

02Instale o kit

Uma vez. Precisa de Node 20 ou mais novo e git. Os blocos das próximas aulas partem desta pasta.

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 as versões do package-lock.json
npm ls --depth=0
Exemplo do repositório
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

Na captura, a pasta pessoal foi trocada por ~; no seu terminal aparece o caminho completo.

03O servidor de turmas

Leia antes de rodar. As duas ferramentas, o recurso e o prompt estão na mesma fábrica; o stdio é só a porta de entrada.

Arquivo · kit/servidor.mjs
// servidor.mjs — o servidor MCP de exemplo do curso (serviço fictício de turmas).
// SDK oficial v2 (@modelcontextprotocol/server 2.3.1, de 05/10/2026), que serve a
// revisão 2026-07-28 do MCP e, pelo mesmo código, clientes da revisão 2025-11-25.
// Não chama modelo nem serviço pago: os dados ficam na memória do processo.
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) Ferramenta só de leitura: o cliente pode aprovar sem perguntar.
  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) Ferramenta com consequência: pede confirmação ao usuário no meio da chamada (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 },
      // Com token (http-auth.mjs), exige o escopo turmas:reservar; sem token (stdio), não 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) Recurso: dado que o cliente lê quando quiser (não é ação).
  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: modelo de pedido que o usuário escolhe no 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;
}
Arquivo · kit/stdio.mjs
// stdio.mjs — serve o servidor de turmas por stdio (o cliente inicia este processo).
// stdout é o canal do protocolo: todo log vai para stderr (console.error).
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { createServer } from './servidor.mjs';

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

04Chame pelo cliente de teste

O teste faz o papel do Claude Code: inicia node stdio.mjs, negocia a revisão e chama buscar_turmas.

Terminal · teste stdio
cd ~/projetos/formacao-webmcp-mcp-v72/kit
node teste-stdio.mjs; echo "saída: $?"
Arquivo · kit/teste-stdio.mjs
// Uso: node teste-stdio.mjs [arquivo do servidor, padrão stdio.mjs]
// teste-stdio.mjs — faz o papel do cliente (Claude Code, Codex…): inicia "node stdio.mjs",
// negocia a revisão do protocolo e chama uma ferramenta pelo stdin/stdout do processo.
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 · erro comum (sem 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]}"
Exemplo do repositório
turmas: servidor MCP no stdio
era: modern · ferramentas: buscar_turmas, reservar_vaga
buscar_turmas → 2 turmas
saída: 0

A primeira linha veio do stderr do servidor; o resto, do cliente.

05Chame pelo MCP Inspector, sem abrir navegador

O Inspector 2.11.0 tem modo linha de comando. Versão fixada no npx para o resultado ser o mesmo da 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: $?"
Exemplo do repositório
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

Sem --cli, o mesmo comando abre a interface web do Inspector, onde você conecta e testa as ferramentas clicando.

06stdout é do protocolo

No stdio, cada linha do stdout é lida como mensagem JSON-RPC. Por isso o kit loga com console.error (stderr). A documentação do SDK avisa que um console.log estraga o canal e o host pode derrubar a conexão.

No nosso teste de 19/10/2026, o cliente do SDK 2.3.1 e o Inspector 2.11.0 pularam uma linha suja e seguiram. Não conte com isso: outro host pode não perdoar. Log sempre no stderr.

Prefere Python? O pacote do SDK oficial mcp 2.3.0 (PyPI, baixado em 19/10/2026) contém server/discover e InputRequiredResult no código; confira as notas de versão do python-sdk antes de usar. Este curso segue em Node; os conceitos valem igual.

07Prática

Ensaio no kit e uma mudança sua.

E3 · Kit instalado

5 min

Rode o bloco instalar.

Pronto quando: npm ls --depth=0 → mostra @modelcontextprotocol/server@2.3.1 e @modelcontextprotocol/client@2.3.1.

E4 · Primeira chamada

5 min

Rode o teste stdio e o Inspector --cli.

Pronto quando: node teste-stdio.mjs → era: modern · ferramentas: buscar_turmas, reservar_vaga e saída: 0; o Inspector devolve um JSON com "structuredContent".

E5 · Uma turma sua

10 min

Acrescente uma turma em TURMAS no servidor.mjs com a palavra mcp no título e rode o teste de novo.

Pronto quando: node teste-stdio.mjs → buscar_turmas → 3 turmas.

08Teste da aula

Pergunta 1 de 4

No transporte stdio, quem inicia o processo do servidor?competência

09Em três frases

Versãonpm ci com o lock do kit: SDK v2 2.3.1.
ServidorUma fábrica createServer; serveStdio é a porta.
TesteCliente do SDK e Inspector --cli, sem host e sem custo.

Para ir além

Aula anteriorPróxima aula