Migrar tu servidor MCP a la spec 2026-07-28: qué se rompe
Tabla de Contenidos
El 28 de julio de 2026 el Model Context Protocol publicó la revisión que más cambia desde que existe: el núcleo del protocolo deja de tener estado. Desaparecen el handshake initialize, el header Mcp-Session-Id y el stream GET; llegan cabeceras obligatorias que el servidor debe validar contra el cuerpo de la petición.
Con casi 9 650 servidores en el registro oficial, hay una cola larga de migraciones por delante. Y en español no hay absolutamente nada escrito sobre esto, así que aquí va el detalle, contrastado contra la especificación y no contra resúmenes de terceros.
Advertencia de caducidad: este artículo es útil durante la ventana de deprecación, que es de doce meses como mínimo. Pasado ese plazo, deja de tener sentido.
Qué cambió, en una pantalla
| Hasta 2025-11-25 | Desde 2026-07-28 | |
|---|---|---|
| Sesión | initialize + Mcp-Session-Id | Sin estado. No hay handshake |
| Metadatos | En la sesión | En cada petición, en _meta |
| Cabeceras HTTP | Opcionales | MCP-Protocol-Version, Mcp-Method, Mcp-Name obligatorias |
| Stream del servidor | GET independiente | subscriptions/listen |
| Peticiones servidor → cliente | JSON-RPC sobre SSE | MRTR: InputRequiredResult |
| Reanudar streams | Last-Event-ID | No soportado |
| Listas | Se repetían siempre | Cacheables con ttlMs |
| Registro de cliente | DCR | CIMD (DCR deprecado) |
| Roots, Sampling, Logging | Vigentes | Deprecados, 12 meses |
| HTTP+SSE (2024-11-05) | Deprecado | Sigue deprecado, y ya retirable |
El motivo de fondo es operativo: sin sesión, cualquier petición puede caer en cualquier instancia detrás de un balanceador sin almacenamiento compartido. MCP pasa a ser una carga HTTP normal.
Lo que desaparece
El handshake initialize
Ya no existe. Antes el cliente abría la conversación con initialize, el servidor respondía capacidades y el cliente confirmaba con initialized. Ese intercambio ataba al cliente a una instancia concreta.
Ahora cada petición viaja completa: lleva su versión de protocolo, la información del cliente y sus capacidades en el propio cuerpo.
Mcp-Session-Id
Fuera, junto con el DELETE que terminaba la sesión. Si tu servidor guardaba estado por sesión, tienes que sacarlo del protocolo: la especificación sugiere pasarlo explícitamente como handles en los argumentos de las tools.
El stream GET
Antes el cliente abría un GET al endpoint para recibir mensajes iniciados por el servidor. Ese endpoint ya no existe; su sustituto es subscriptions/listen, que veremos abajo.
Last-Event-ID
Los streams ya no son reanudables. Si tu implementación dependía de reanudar tras una desconexión, hay que rediseñar esa parte.
Las cabeceras obligatorias (y la trampa)
Esta es la parte que rompe en silencio, así que va con detalle.
Toda petición POST al endpoint MCP debe llevar:
| Cabecera | De dónde sale | Obligatoria en |
|---|---|---|
MCP-Protocol-Version | — | Todas |
Mcp-Method | method | Todas |
Mcp-Name | params.name o params.uri | tools/call, resources/read, prompts/get |
Una petición real, tal como la define la especificación:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
El propósito es que un balanceador o un rate limiter pueda enrutar y medir sin parsear el JSON. Muy sensato.
La trampa: las cabeceras deben coincidir con el cuerpo
Aquí es donde se cae la mitad de las migraciones. El servidor debe rechazar cualquier petición donde una cabecera no coincida con su campo equivalente en el cuerpo:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32020,
"message": "Header mismatch: Mcp-Name header value 'foo' does not match body value 'bar'"
}
}
400 Bad Request con el código -32020 (HeaderMismatch). Y no es opcional: es un requisito de seguridad. Si el balanceador enruta por la cabecera y el servidor ejecuta según el cuerpo, un atacante que las descuadre consigue que la petición se enrute a un sitio y se ejecute como otra cosa.
Condiciones que obligan a rechazar:
- Falta una cabecera obligatoria.
- El valor de la cabecera no coincide con el del cuerpo.
- El valor contiene caracteres inválidos.
Un detalle fácil de pasar por alto: los valores enteros hay que compararlos numéricamente, no como cadenas — 42 y 42.0 son iguales.
Para depurar esto, lo práctico es lanzar la petición a mano y ver qué contesta tu servidor antes de conectar ningún cliente:
Cuando el valor no cabe en una cabecera
Las cabeceras HTTP solo admiten ASCII visible. Si un nombre de tool o una URI llevan acentos, saltos de línea o espacios al principio, hay que codificarlos con un formato centinela:
Mcp-Name: =?base64?SGVsbG8sIOS4lueVjA==?=
El prefijo =?base64? y el sufijo ?= son sensibles a mayúsculas y deben aparecer exactamente así. El servidor tiene que decodificar antes de comparar con el cuerpo.
Y hay un caso rebuscado que conviene tener presente: si un valor ASCII normal resulta empezar por =?base64? y terminar en ?=, hay que codificarlo igualmente para que no se confunda con el centinela.
En español y portugués esto no es un caso raro: cualquier URI de recurso con una ñ o una tilde entra por aquí.
MRTR: cómo se pide algo al usuario ahora
Antes, si una tool necesitaba una decisión del usuario a mitad de ejecución, el servidor emitía su propia petición JSON-RPC por el stream SSE. Eso ya no está permitido: los servidores no inician peticiones.
El sustituto se llama Multi Round-Trip Requests. El servidor devuelve un resultado especial y el cliente vuelve a llamar:
sequenceDiagram
participant C as Cliente
participant S as Servidor
C->>S: POST tools/call (id: 1)
Note over S: Necesita input del usuario
S-->>C: InputRequiredResult<br/>(inputRequests)
Note over C: Recoge lo que le piden
C->>S: POST tools/call (id: 2)<br/>params originales + inputResponses
S-->>C: Resultado final
En la práctica: el servidor responde con resultType: "input_required" e inputRequests, y el cliente reintenta la petición original añadiendo inputResponses.
Esto afecta a sampling, elicitation y roots, que antes eran peticiones del servidor y ahora son campos dentro de un resultado. Si tu servidor usaba cualquiera de las tres, es la reescritura más grande de la migración — y ojo, porque el servidor ahora tiene que ser capaz de reanudar el trabajo desde una segunda llamada, no desde una conexión abierta.
subscriptions/listen para notificaciones
Las notificaciones de cambio (notifications/tools/list_changed, notifications/resources/updated) ya no llegan por un stream GET. El cliente abre un stream largo con una petición:
sequenceDiagram
participant C as Cliente
participant S as Servidor
C->>S: POST subscriptions/listen<br/>(filtro de notificaciones)
S-->>C: SSE: subscriptions/acknowledged
Note over C,S: El stream queda abierto
S-->>C: SSE: tools/list_changed
S-->>C: SSE: resources/updated
Dos cosas que la especificación deja claras y que conviene no confundir:
- Las notificaciones de la petición (
notifications/progress,notifications/message) viajan por el stream de esa petición, no por el delisten. - En streams largos conviene emitir periódicamente un comentario SSE (una línea que empieza por dos puntos,
:\r\n) como keep-alive, para que ningún intermediario cierre la conexión en los ratos tranquilos.
Y si sirves SSE detrás de nginx, la cabecera que evita que el proxy acumule eventos en un búfer:
X-Accel-Buffering: no
Listas cacheables
tools/list, prompts/list, resources/list y resources/read ahora pueden declarar cuánto vale su respuesta:
ttlMs— cuánto tiempo puede cachearsecacheScope— con qué alcance
Es la mejora más barata de adoptar y la que más tráfico ahorra: el catálogo de tools se repetía en cada arranque de cliente sin ningún motivo.
Autorización: de DCR a CIMD
El registro dinámico de clientes (DCR) queda deprecado en favor de los Client ID Metadata Documents (CIMD). DCR sigue funcionando durante la ventana de doce meses.
Tres endurecimientos que acompañan al cambio:
- Los servidores deben devolver el parámetro
issconforme a la RFC 9207, y los clientes deben validarlo antes de canjear el código de autorización. - Las credenciales de cliente quedan ligadas al servidor de autorización que las emitió, lo que impide reutilizarlas contra otro.
- Los clientes declaran
application_typeal registrarse, que es lo que por fin resuelve los errores de redirección alocalhosten aplicaciones de escritorio y CLI.
Deprecados con doce meses de plazo
Tres funciones entran en cuenta atrás:
- Roots
- Sampling
- Logging
Y el transporte HTTP+SSE de 2024-11-05, que ya estaba deprecado desde 2025-03-26, entra en su rampa final: la especificación lo declara elegible para eliminación en una revisión futura.
Si tu servidor sigue exponiendo el par de endpoints SSE + POST del transporte viejo, ese es el trabajo que no puedes aplazar mucho más.
Cómo atender a los clientes que aún no migraron
Un servidor que solo hable la revisión nueva y reciba tráfico antiguo debe comportarse así:
| Recibe | Responde |
|---|---|
GET o DELETE al endpoint MCP | 405 Method Not Allowed |
Cabecera Mcp-Session-Id | Ignorarla. No generar ni devolver IDs de sesión |
Cabecera Last-Event-ID | Ignorarla; los streams no son reanudables |
| Método que no implementa | 404 + JSON-RPC -32601 |
| Versión de protocolo no soportada | 400 + UnsupportedProtocolVersionError con la lista de versiones que sí soporta |
Ese 404 con cuerpo JSON-RPC tiene un motivo concreto: distingue «no implemento ese método» de un 404 de un servidor HTTP+SSE antiguo que ni siquiera tiene el endpoint moderno.
La detección funciona al revés en el cliente: intenta primero lo moderno y, si recibe un 400, mira el cuerpo antes de rendirse. Si trae un error JSON-RPC reconocible, el servidor es moderno y hay que corregir la petición; si viene vacío o ilegible, entonces sí toca caer a initialize.
Seguridad, que sigue igual de olvidada
La especificación insiste en tres cosas que no son nuevas pero se siguen incumpliendo:
- Validar la cabecera
Originen toda conexión entrante, y devolver403 Forbiddensi es inválida. Sin esto, una web cualquiera puede hacer DNS rebinding contra tu servidor MCP local. - Escuchar solo en
127.0.0.1cuando corre en local, nunca en0.0.0.0. - Autenticar todas las conexiones.
El primero es el que más se salta, y es el que convierte un servidor MCP de desarrollo en una puerta abierta desde el navegador de quien visite la página equivocada.
Estado de los SDK, y qué pasa con PHP
Los SDK oficiales Tier 1 ya soportan 2026-07-28: TypeScript, Python, Go y C#. El de Rust va en beta.
PHP no está en esa lista, ni en Tier 1 ni con soporte anunciado. Si tienes un servidor MCP en Laravel con el paquete laravel/mcp —como el que expliqué en cómo construir un agente de IA con Laravel y MCP—, la buena noticia es que el paquete abstrae el protocolo y la mayor parte de este cambio no te toca el código de las tools.
La mala es que dependes de que el paquete se actualice para hablar la revisión nueva, y de que tu proyecto siga usando el transporte SSE mientras tanto. Merece la pena fijar la versión y vigilar el repositorio antes de que los clientes empiecen a exigir la spec nueva.
Checklist de migración
- Fuera el handshake
initialize/initialized - Fuera
Mcp-Session-Idy elDELETEde fin de sesión - Estado de sesión convertido en handles explícitos en los argumentos
- Endpoint GET eliminado;
GET/DELETEdevuelven405 -
MCP-Protocol-Version,Mcp-MethodyMcp-Nameleídas y validadas contra el cuerpo - Error
-32020implementado con400 - Decodificación del centinela
=?base64?…?=antes de comparar - Sampling, elicitation y roots migrados a MRTR
-
subscriptions/listenimplementado para notificaciones de cambio -
X-Accel-Buffering: noy keep-alive en streams largos -
ttlMsycacheScopedeclarados en las respuestas de lista - Validación de
Origincon403 - Plan para DCR → CIMD antes de que cierre la ventana
- Transporte HTTP+SSE con fecha de retirada puesta en el calendario
Conclusión
Esta revisión no añade funciones: quita las que impedían escalar. Un protocolo con sesión obliga a pegar cada cliente a una instancia; sin ella, un servidor MCP se pone detrás de un balanceador de reparto circular y ya está.
El coste de esa simplificación se paga en tres sitios: la sesión hay que reconstruirla como handles explícitos, sampling y roots hay que rehacerlos con MRTR, y hay que validar cabeceras contra el cuerpo, que es el requisito nuevo que más gente va a implementar mal o directamente ignorar.
Y sobre las prisas: la ventana de deprecación es de doce meses como mínimo, así que no hay incendio. Pero si tu servidor todavía vive en el transporte HTTP+SSE de 2024, ese sí lleva dos revisiones deprecado y ya es elegible para eliminación.