E3 · Kit installed
5 minRun the install block.
Done when: npm ls --depth=0 → shows @modelcontextprotocol/server@2.3.1 and @modelcontextprotocol/client@2.3.1.
Lesson 2 · 20 min
The simplest way to connect a service to an agent is stdio: the client starts your program and talks to it over stdin and stdout. In this lesson you install the kit with pinned versions, read the class server, and call the tool two ways: with a test client and with the MCP Inspector in command-line mode.
Start the lesson
Goal: start an MCP server over stdio with the official v2 SDK and call it with a test client and with the Inspector, no host at all.
Click each step.
Once. You need Node 20 or newer and git. The blocks in the next lessons start from this folder.
git clone https://github.com/inematds/formacao-webmcp-mcp-v72 ~/projetos/formacao-webmcp-mcp-v72 cd ~/projetos/formacao-webmcp-mcp-v72/kit npm ci # installs the versions from 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
In the capture, the home folder was replaced with ~; in your terminal the full path appears.
Read before you run. The two tools, the resource, and the prompt are in the same factory; stdio is just the front door.
// servidor.mjs — the course's example MCP server (a fictional class service).
// Official SDK v2 (@modelcontextprotocol/server 2.3.1, from 05/10/2026), which serves the
// 2026-07-28 MCP revision and, with the same code, clients on the 2025-11-25 revision.
// It calls no model or paid service: the data stays in the process memory.
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) Read-only tool: the client can approve it without asking.
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 with consequences: asks the user for confirmation mid-call (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 },
// With a token (http-auth.mjs), it requires the turmas:reservar scope; without a token (stdio), it does not apply.
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: data the client reads whenever it wants (not an action).
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: a request template the user picks in the client.
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 — serves the classes server over stdio (the client starts this process).
// stdout is the protocol channel: every log goes to stderr (console.error).
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { createServer } from './servidor.mjs';
serveStdio(createServer);
console.error('turmas: servidor MCP no stdio');The test plays the role of Claude Code: it starts node stdio.mjs, negotiates the revision, and calls buscar_turmas.
cd ~/projetos/formacao-webmcp-mcp-v72/kit node teste-stdio.mjs; echo "saída: $?"
// Usage: node teste-stdio.mjs [server file, default stdio.mjs]
// teste-stdio.mjs — plays the role of the client (Claude Code, Codex…): starts "node stdio.mjs",
// negotiates the protocol revision, and calls a tool through the process's stdin/stdout.
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
The first line came from the server's stderr; the rest came from the client.
ERR_MODULE_NOT_FOUND saída: 1
Captured with the "common error" block above, which copies the files to an empty folder, without node_modules. Fix: run npm ci inside kit/.
Inspector 2.11.0 has a command-line mode. The version is pinned in npx so the result matches the capture.
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: 0Without --cli, the same command opens the Inspector web interface, where you connect and test the tools by clicking.
On stdio, every line of stdout is read as a JSON-RPC message. That's why the kit logs with console.error (stderr). The SDK documentation warns that a console.log corrupts the channel and the host may drop the connection.
In our test on 10/19/2026, the SDK 2.3.1 client and Inspector 2.11.0 skipped a stray line and kept going. Don't count on that: another host may not be so forgiving. Always log to stderr.
Prefer Python? The official SDK package mcp 2.3.0 (PyPI, downloaded on 10/19/2026) contains server/discover and InputRequiredResult in its code; check the python-sdk release notes before using it. This course stays with Node; the concepts apply equally.
Rehearsal on the kit and one change of your own.
Run the install block.
Done when: npm ls --depth=0 → shows @modelcontextprotocol/server@2.3.1 and @modelcontextprotocol/client@2.3.1.
Run the stdio test and the Inspector --cli.
Done when: node teste-stdio.mjs → era: modern · ferramentas: buscar_turmas, reservar_vaga and saída: 0; the Inspector returns a JSON with "structuredContent".
Add a class to TURMAS in servidor.mjs with the word mcp in the title and run the test again.
Done when: node teste-stdio.mjs → buscar_turmas → 3 turmas.
Question 1 of 4
On the stdio transport, who starts the server process?skill
Question 2 of 4
Why does the kit use npm ci instead of npm install?skill
npm ci guarantees those versions. npm install may move up to a version that behaves differently.npm ci guarantees those versions. npm install may move up to a version that behaves differently.Review: Install the kitQuestion 3 of 4
Where should the stdio server write its logs?skill
Question 4 of 4
The test shows era: modern. What does that guarantee?
mode: 'auto' and the server offered 2026-07-28. An older client is still served by the same server (lesson 4).mode: 'auto' and the server offered 2026-07-28. An older client is still served by the same server (lesson 4).Review: Install the kitPick an option. The explanation appears after you try.