E9 · discover
5 minEjecuta el bloque server/discover.
Listo cuando: la salida tiene "supportedVersions":["2026-07-28"] y termina en saída: 0. ¿Salió saída: 1? Revisa que el puerto 3333 esté libre (ss -ltn | grep 3333).
Clase 4 · 20 min
Para que el servicio atienda a varios clientes en una URL, el transporte es Streamable HTTP. En la revisión 2026-07-28 quedó sin estado: no hay sesión que guardar, cada solicitud lleva lo que necesita. En esta lección levantas el mismo servidor por HTTP y hablas con él con curl, incluso equivocándote a propósito.
Comenzar la lección
Objetivo: servir el mismo servidor por Streamable HTTP sin estado y leer server/discover y los encabezados Mcp-Method y Mcp-Name en una solicitud hecha a mano.
Haz clic en cada etapa.
Los tutoriales de 2025 enseñan SSE como transporte propio, un initialize al comienzo y el encabezado Mcp-Session-Id para amarrar los pedidos de una sesión. En la 2026-07-28 eso salió: sin sesión, sin initialize, y el GET/SSE para recibir avisos pasó a ser subscriptions/listen (changelog oficial, leído el 19/10/2026).
Sin estado, el servidor escala como cualquier API: dos instancias detrás de un balanceador atienden igual. Lo que tenga que sobrevivir entre pedidos son datos tuyos, guardados por ti.
La misma fábrica de la clase 2. Escucha solo en 127.0.0.1.
// http.mjs — sirve el servidor de turmas por Streamable HTTP, sin estado, en 127.0.0.1.
// Cada solicitud monta un McpServer nuevo (createMcpHandler): el protocolo no guarda sesión.
// Los datos de tu servicio (aquí, TURMAS en el módulo) siguen siendo tuyos: base de datos, caché, pool quedan fuera de la fábrica.
// Las guardas de Host y Origin frenan el DNS rebinding (una página maliciosa hablando con tu 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 por sí solo
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 hecho a mano. El bloque levanta el servidor, pregunta y lo apaga.
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 dice las revisiones que habla; capabilities, lo que ofrece. ttlMs y cacheScope son las pistas de caché de la revisión nueva.
El segundo pedido es el mismo, sin el encabezado. El servidor lo rechaza porque encabezado y cuerpo no coinciden.
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 del encabezado Mcp-Name (vacío = sin el encabezado)
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]Un cliente hecho con el SDK manda los tres encabezados solo. El error -32020 aparece en clientes escritos a mano o en proxies que quitan encabezados.
Un pedido initialize de la revisión 2025-11-25 al mismo servidor.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
C=${TMPDIR:-/tmp}/cabecalhos-mcp.txt
# un cliente de la revisión 2025-11-25 todavía abre con initialize; el mismo 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
El handler del SDK v2 atiende la era 2025 sin estado y no devuelve Mcp-Session-Id. No necesitas dos servidores durante la transición.
Tres pedidos a mano y un ajuste tuyo.
Ejecuta el bloque server/discover.
Listo cuando: la salida tiene "supportedVersions":["2026-07-28"] y termina en saída: 0. ¿Salió saída: 1? Revisa que el puerto 3333 esté libre (ss -ltn | grep 3333).
Ejecuta el bloque tools/call a mano.
Listo cuando: la primera llamada da [HTTP 200] con "structuredContent"; la segunda, [HTTP 400] con "code":-32020.
Levanta el servidor con PORTA=4000 node http.mjs y repite el discover cambiando el puerto en el curl.
Listo cuando: el curl a http://127.0.0.1:4000/mcp devuelve "supportedVersions".
Pregunta 1 de 4
En la revisión 2026-07-28, ¿cómo sabe el servidor la versión y las capacidades del cliente?competencia
_meta. initialize y Mcp-Session-Id son de la era 2025._meta. initialize y Mcp-Session-Id son de la era 2025.Repasar: Una solicitud, del curl a la respuestaPregunta 2 de 4
¿Qué resultado devuelve un tools/call sin el encabezado Mcp-Name?competencia
Mcp-Name en un método que tiene nombre, lo rechaza con 400 y -32020 (encabezado y cuerpo discrepan).Mcp-Name en un método que tiene nombre, lo rechaza con 400 y -32020 (encabezado y cuerpo discrepan).Repasar: El servidor HTTP del kitPregunta 3 de 4
¿Para qué sirven las guardas de Host y Origin en el servidor en 127.0.0.1?
Pregunta 4 de 4
Un cliente antiguo envía initialize (2025-11-25) al servidor del kit. ¿Qué pasa?
protocolVersion 2025-11-25 y cero Mcp-Session-Id.protocolVersion 2025-11-25 y cero Mcp-Session-Id.Repasar: El servidor HTTP del kitElige una alternativa. La explicación aparece después de tu intento.