E9 · discover
5 minRode o bloco server/discover.
Pronto quando: a saída tem "supportedVersions":["2026-07-28"] e termina em saída: 0. Deu saída: 1? Veja se a porta 3333 está livre (ss -ltn | grep 3333).
Aula 4 · 20 min
Para o serviço atender vários clientes numa URL, o transporte é o Streamable HTTP. Na revisão 2026-07-28 ele ficou sem estado: nenhuma sessão para guardar, cada pedido carrega o que precisa. Nesta aula você sobe o mesmo servidor por HTTP e conversa com ele com curl, inclusive errando de propósito.
Começar a aula
Objetivo: servir o mesmo servidor por Streamable HTTP sem estado e ler server/discover e os cabeçalhos Mcp-Method e Mcp-Name num pedido feito à mão.
Clique em cada etapa.
Tutoriais de 2025 ensinam SSE como transporte próprio, um initialize no começo e o cabeçalho Mcp-Session-Id para amarrar os pedidos de uma sessão. Na 2026-07-28 isso saiu: sem sessão, sem initialize, e o GET/SSE para receber avisos virou subscriptions/listen (changelog oficial, lido em 19/10/2026).
Sem estado, o servidor escala como qualquer API: duas instâncias atrás de um balanceador atendem igual. O que precisar sobreviver entre pedidos é dado seu, guardado por você.
Mesma fábrica da aula 2. Escuta só em 127.0.0.1.
// http.mjs — serve o servidor de turmas por Streamable HTTP, sem estado, em 127.0.0.1.
// Cada requisição monta um McpServer novo (createMcpHandler): o protocolo não guarda sessão.
// O dado do seu serviço (aqui, TURMAS no módulo) continua seu: banco, cache, pool ficam fora da fábrica.
// As guardas de Host e Origin barram DNS rebinding (página maliciosa falando com o seu localhost).
import { createServer as criarHttp } from 'node:http';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler, localhostHostValidation, localhostOriginValidation } from '@modelcontextprotocol/node';
import { createServer } from './servidor.mjs';
const PORTA = Number(process.env.PORTA ?? 3333);
const handler = createMcpHandler(createServer);
const node = toNodeHandler(handler);
const hostOk = localhostHostValidation();
const origemOk = localhostOriginValidation();
const http = criarHttp((req, res) => {
if (!hostOk(req, res) || !origemOk(req, res)) return; // responde 403 sozinho
if (!req.url.startsWith('/mcp')) { res.writeHead(404).end(); return; }
void node(req, res);
});
http.listen(PORTA, '127.0.0.1', () => console.error(`turmas: MCP em http://127.0.0.1:${PORTA}/mcp`));
process.on('SIGINT', async () => { await handler.close(); http.close(); process.exit(0); });
process.on('SIGTERM', async () => { await handler.close(); http.close(); process.exit(0); });server/discover feito à mão. O bloco sobe o servidor, pergunta e desliga.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
curl -s -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{}}}}' | grep '"supportedVersions"'
S=$?; kill $PID; wait $PID 2>/dev/null; echo "saída: $S"turmas: MCP em http://127.0.0.1:3333/mcp
{"result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"prompts":{"listChanged":true}},"resultType":"complete","ttlMs":0,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"turmas","version":"1.0.0"}}},"jsonrpc":"2.0","id":1}
saída: 0supportedVersions diz as revisões que ele fala; capabilities, o que oferece. ttlMs e cacheScope são as dicas de cache da revisão nova.
O segundo pedido é o mesmo, sem o cabeçalho. O servidor recusa porque cabeçalho e corpo não batem.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}'
chamar() { # $1 = valor do cabeçalho Mcp-Name (vazio = sem o cabeçalho)
curl -s -w ' [HTTP %{http_code}]\n' -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' ${1:+-H "Mcp-Name: $1"} \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"buscar_turmas","arguments":{"termo":"mcp"},'"$META"'}}'
}
echo "## com Mcp-Name"; chamar buscar_turmas
echo "## sem Mcp-Name"; chamar ""
kill $PID; wait $PID 2>/dev/nullturmas: MCP em http://127.0.0.1:3333/mcp
## com Mcp-Name
{"result":{"content":[{"type":"text","text":"mcp-01 · Servidor MCP em uma tarde · 1 vagas\nmcp-02 · MCP com autorização · 0 vagas"}],"structuredContent":{"turmas":[{"id":"mcp-01","titulo":"Servidor MCP em uma tarde","vagas":1},{"id":"mcp-02","titulo":"MCP com autorização","vagas":0}]},"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"turmas","version":"1.0.0"}}},"jsonrpc":"2.0","id":2} [HTTP 200]
## sem Mcp-Name
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body carries params.name=\"buscar_turmas\" but the required Mcp-Name header is absent","data":{"mismatch":{"header":"(missing)","body":"the body carries params.name=\"buscar_turmas\" but the required Mcp-Name header is absent"}}},"id":2} [HTTP 400]Cliente feito com o SDK manda os três cabeçalhos sozinho. O erro -32020 aparece em cliente escrito à mão ou em proxy que remove cabeçalhos.
Um pedido initialize da revisão 2025-11-25 no mesmo servidor.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
C=${TMPDIR:-/tmp}/cabecalhos-mcp.txt
# um cliente da revisão 2025-11-25 ainda abre com initialize; o mesmo servidor responde
curl -s -D "$C" -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"antigo","version":"1.0.0"}}}' \
| grep -o '"protocolVersion":"[^"]*"'
echo "Mcp-Session-Id na resposta: $(grep -ci '^mcp-session-id' "$C")"
rm -f "$C"; kill $PID; wait $PID 2>/dev/nullturmas: MCP em http://127.0.0.1:3333/mcp "protocolVersion":"2025-11-25" Mcp-Session-Id na resposta: 0
O handler do SDK v2 atende a era 2025 sem estado e não devolve Mcp-Session-Id. Você não precisa de dois servidores durante a transição.
Três pedidos à mão e um ajuste seu.
Rode o bloco server/discover.
Pronto quando: a saída tem "supportedVersions":["2026-07-28"] e termina em saída: 0. Deu saída: 1? Veja se a porta 3333 está livre (ss -ltn | grep 3333).
Rode o bloco tools/call à mão.
Pronto quando: primeira chamada [HTTP 200] com "structuredContent"; segunda [HTTP 400] com "code":-32020.
Suba o servidor com PORTA=4000 node http.mjs e repita o discover trocando a porta no curl.
Pronto quando: o curl em http://127.0.0.1:4000/mcp devolve "supportedVersions".
Pergunta 1 de 4
Na revisão 2026-07-28, como o servidor sabe a versão e as capacidades do cliente?competência
_meta. initialize e Mcp-Session-Id são da era 2025._meta. initialize e Mcp-Session-Id são da era 2025.Rever: Um pedido, do curl à respostaPergunta 2 de 4
O tools/call sem o cabeçalho Mcp-Name volta com qual resultado?competência
Mcp-Name num método que tem nome, recusa com 400 e -32020 (cabeçalho e corpo discordam).Mcp-Name num método que tem nome, recusa com 400 e -32020 (cabeçalho e corpo discordam).Rever: O servidor HTTP do kitPergunta 3 de 4
Para que servem as guardas de Host e Origin no servidor em 127.0.0.1?
Pergunta 4 de 4
Um cliente antigo manda initialize (2025-11-25) para o servidor do kit. O que acontece?
protocolVersion 2025-11-25 e zero Mcp-Session-Id.protocolVersion 2025-11-25 e zero Mcp-Session-Id.Rever: O servidor HTTP do kitEscolha uma alternativa. A explicação aparece depois da sua tentativa.