E3 · Kit instalado
5 minRode o bloco instalar.
Pronto quando: npm ls --depth=0 → mostra @modelcontextprotocol/server@2.3.1 e @modelcontextprotocol/client@2.3.1.
Aula 2 · 20 min
O jeito mais simples de ligar um serviço a um agente é o stdio: o cliente inicia o seu programa e conversa pelo stdin e stdout. Nesta aula você instala o kit com versões fixas, lê o servidor de turmas e chama a ferramenta de dois jeitos: por um cliente de teste e pelo MCP Inspector em modo linha de comando.
Começar a aula
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.
Clique em cada etapa.
Uma vez. Precisa de Node 20 ou mais novo e git. Os blocos das próximas aulas partem desta pasta.
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
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.
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.
// 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;
}// 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');O teste faz o papel do Claude Code: inicia node stdio.mjs, negocia a revisão e chama buscar_turmas.
cd ~/projetos/formacao-webmcp-mcp-v72/kit node teste-stdio.mjs; echo "saída: $?"
// 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();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]}"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.
ERR_MODULE_NOT_FOUND saída: 1
Capturado com o bloco "erro comum" acima, que copia os arquivos para uma pasta vazia, sem node_modules. Solução: npm ci dentro de kit/.
O Inspector 2.11.0 tem modo linha de comando. Versão fixada no npx para o resultado ser o mesmo da captura.
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: $?"
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: 0Sem --cli, o mesmo comando abre a interface web do Inspector, onde você conecta e testa as ferramentas clicando.
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.
Ensaio no kit e uma mudança sua.
Rode o bloco instalar.
Pronto quando: npm ls --depth=0 → mostra @modelcontextprotocol/server@2.3.1 e @modelcontextprotocol/client@2.3.1.
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".
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.
Pergunta 1 de 4
No transporte stdio, quem inicia o processo do servidor?competência
Pergunta 2 de 4
Por que o kit usa npm ci em vez de npm install?competência
npm ci garante essas versões. npm install pode subir para uma versão que se comporta diferente.npm ci garante essas versões. npm install pode subir para uma versão que se comporta diferente.Rever: Instale o kitPergunta 3 de 4
Onde o servidor stdio deve escrever os logs?competência
Pergunta 4 de 4
O teste mostra era: modern. O que isso garante?
mode: 'auto' e o servidor ofereceu 2026-07-28. Cliente antigo continua atendido pelo mesmo servidor (aula 4).mode: 'auto' e o servidor ofereceu 2026-07-28. Cliente antigo continua atendido pelo mesmo servidor (aula 4).Rever: Instale o kitEscolha uma alternativa. A explicação aparece depois da sua tentativa.