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

Nesta aula você vai

  1. 1A mesma fábrica por HTTP com createMcpHandler
  2. 2server/discover: o que o servidor anuncia
  3. 3Cabeçalhos obrigatórios e o erro -32020 quando faltam
  4. 4Guardas de Host e Origin no localhost; cliente antigo ainda atendido

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.

01Um pedido, do curl à resposta

Clique em cada etapa.

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

02O que saiu do HTTP de 2025

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ê.

03O servidor HTTP do kit

Mesma fábrica da aula 2. Escuta só em 127.0.0.1.

Arquivo · kit/http.mjs
// 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); });

04Pergunte ao servidor o que ele oferece

server/discover feito à mão. O bloco sobe o servidor, pergunta e desliga.

Terminal · server/discover
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"
Exemplo do repositório
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: 0

supportedVersions diz as revisões que ele fala; capabilities, o que oferece. ttlMs e cacheScope são as dicas de cache da revisão nova.

05Chame uma ferramenta, com e sem Mcp-Name

O segundo pedido é o mesmo, sem o cabeçalho. O servidor recusa porque cabeçalho e corpo não batem.

Terminal · tools/call à mão
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/null
Exemplo do repositório
turmas: 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.

06Cliente antigo continua atendido

Um pedido initialize da revisão 2025-11-25 no mesmo servidor.

Terminal · cliente 2025
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/null
Exemplo do repositório
turmas: 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.

07Prática

Três pedidos à mão e um ajuste seu.

E9 · discover

5 min

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).

E10 · O erro de cabeçalho

5 min

Rode o bloco tools/call à mão.

Pronto quando: primeira chamada [HTTP 200] com "structuredContent"; segunda [HTTP 400] com "code":-32020.

E11 · Outra porta

5 min

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".

08Teste da aula

Pergunta 1 de 4

Na revisão 2026-07-28, como o servidor sabe a versão e as capacidades do cliente?competência

09Em três frases

Sem estadoCada pedido leva _meta; nada de sessão nem initialize.
CabeçalhosMCP-Protocol-Version, Mcp-Method e Mcp-Name espelham o corpo.
LocalhostGuardas de Host e Origin na frente do handler.

Para ir além

Aula anteriorPróxima aula