Servidor MCP de tu servicio|INEMA.CLUB-PRO

Clase 4 · 20 min

Streamable HTTP sin estado: server/discover, encabezados Mcp-Method y Mcp-Name

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
Rack de red oscuro con varios cables conectados a un switch, pequeñas luces blancas.

En esta lección vas a

  1. 1La misma fábrica por HTTP con createMcpHandler
  2. 2server/discover: lo que el servidor anuncia
  3. 3Encabezados obligatorios y el error -32020 cuando faltan
  4. 4Guardas de Host y Origin en localhost; cliente antiguo aún atendido

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.

01Una solicitud, del curl a la respuesta

Haz clic en cada etapa.

Haz clic en cada bloque del diagrama para ver qué hace.

02Lo que salió del HTTP de 2025

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.

03El servidor HTTP del kit

La misma fábrica de la clase 2. Escucha solo en 127.0.0.1.

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

04Pregúntale al servidor qué ofrece

server/discover hecho a mano. El bloque levanta el servidor, pregunta y lo apaga.

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"
Ejemplo del repositorio
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 dice las revisiones que habla; capabilities, lo que ofrece. ttlMs y cacheScope son las pistas de caché de la revisión nueva.

05Llama una tool, con y sin Mcp-Name

El segundo pedido es el mismo, sin el encabezado. El servidor lo rechaza porque encabezado y cuerpo no coinciden.

Terminal · tools/call a mano
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/null
Ejemplo del repositorio
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]

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.

06Un cliente antiguo sigue siendo atendido

Un pedido initialize de la revisión 2025-11-25 al mismo 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
# 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/null
Ejemplo del repositorio
turmas: 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.

07Práctica

Tres pedidos a mano y un ajuste tuyo.

E9 · discover

5 min

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

E10 · El error de encabezado

5 min

Ejecuta el bloque tools/call a mano.

Listo cuando: la primera llamada da [HTTP 200] con "structuredContent"; la segunda, [HTTP 400] con "code":-32020.

E11 · Otro puerto

5 min

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

08Prueba de la clase

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

09En tres frases

Sin estadoCada solicitud lleva _meta; no hay sesión ni initialize.
EncabezadosMCP-Protocol-Version, Mcp-Method y Mcp-Name reflejan el cuerpo.
LocalhostGuardas de Host y Origin delante del handler.

Para ir más allá

Clase anteriorSiguiente clase