Servidores MCP sin estado: qué cambió realmente
La revisión MCP 2026-07-28 eliminó las sesiones y el handshake initialize. Ajuste proxy inverso, health checks, timeouts y autenticación del servidor.
Qué es un servidor MCP sin estado
Un servidor MCP sin estado no conserva el estado de cada cliente entre solicitudes. Cada solicitud incluye la versión del protocolo, las capacidades del cliente y las credenciales que el servidor necesita para responder, por lo que cualquier proceso en cualquier máquina puede responder a cualquier solicitud. MCP (Model Context Protocol, el formato de comunicación que usan los agentes para acceder a herramientas) convirtió esto en una regla en la revisión 2026-07-28, que eliminó el handshake initialize y la sesión HTTP subyacente. Todo lo explicado aquí corresponde al lado servidor de esa comunicación. Si el lado del agente todavía es nuevo para usted, una ruta progresiva para aprender sobre agentes de IA explica el ciclo que decide llamar a una herramienta antes de que importe cualquiera de estos detalles HTTP.
Ese es el objetivo operativo. Un servidor que no conserva datos por cliente puede situarse detrás de un balanceador de carga normal sin afinidad de sesión, reiniciarse durante un despliegue sin interrumpir a los clientes y ejecutarse como cuatro procesos idénticos en lugar de uno. Un servidor orientado a sesiones no ofrece ninguna de esas ventajas sin mecanismos adicionales.
Model Context Protocol es un protocolo sin estado: toda la información necesaria para procesar una solicitud está contenida en la propia solicitud. El servidor procesa cada solicitud de forma independiente. No se debe inferir ningún estado de solicitudes anteriores, ni siquiera de las realizadas en la misma conexión o transmisión.
Stateless no significa que el servidor no almacene nada. La base de datos, la cola y la caché siguen existiendo. Significa que el protocolo no mantiene estado en la conexión, por lo que el servidor no debe tratar una conexión, un proceso o un socket abierto como sustituto de «este cliente, en mitad de una conversación». La diferencia se ve con más claridad en una aplicación que ya gestiona sus propios datos: servidor MCP de solo lectura de openGym responde a preguntas sobre el historial de entrenamiento almacenado en la base de datos de la propia aplicación, y nada relacionado con ese almacenamiento depende de la conexión por la que llegó una solicitud concreta.
Qué eliminó la revisión 2026-07-28
2026-07-28 es la revisión actual de la especificación a fecha de agosto de 2026. En comparación con 2025-11-25, elimina cinco elementos que existían para admitir sesiones.
- La solicitud
initializey la notificaciónnotifications/initialized. No hay ningún handshake (SEP-2575). - La cabecera
Mcp-Session-Idy la terminación de sesiones con HTTPDELETE(SEP-2567). - El flujo HTTP independiente
GETque los servidores usaban para enviar notificaciones. Se sustituye porsubscriptions/listen, un POST normal cuya respuesta es un flujo de larga duración. - La posibilidad de reanudar flujos SSE (eventos enviados por el servidor). La cabecera
Last-Event-IDy los identificadores por evento desaparecen. Si se interrumpe un flujo, se pierde la solicitud en curso y el cliente debe volver a emitirla como una solicitud nueva con un identificador de solicitud nuevo. ping,logging/setLevelynotifications/roots/list_changed. El nivel de registro es ahora un campo por solicitud,io.modelcontextprotocol/logLevelen_meta.
Se añadió un método y todos los servidores deben implementarlo. server/discover devuelve en una sola llamada las versiones de protocolo compatibles con el servidor, sus capacidades y su identidad. Es lo más parecido a un handshake que queda, y los clientes no están obligados a llamarlo.
Por qué era difícil ejecutar el transporte basado en sesiones en producción
En 2025-11-25 y versiones anteriores, un servidor podía generar un ID de sesión durante la inicialización y devolverlo en la cabecera Mcp-Session-Id de la InitializeResult. Después, el cliente debía enviar esa cabecera en todas las solicitudes posteriores. La versión de protocolo negociada y las capacidades del cliente se almacenaban en la memoria del servidor, asociadas a ese ID. Cada una de estas decisiones tenía un coste operativo.
- Un reinicio eliminaba la tabla de sesiones. La especificación exigía que el servidor respondiera con
404 Not Founda cualquier solicitud que incluyera un ID de sesión caducado, y que el cliente comenzara de nuevo con unInitializeRequestnuevo. Cada despliegue provocaba una reconexión de todos los clientes conectados. - Una segunda réplica no conocía las sesiones de la primera. Escalar horizontalmente requería enrutamiento persistente en el balanceador de carga o un almacén de sesiones compartido que todas las réplicas consultaran en cada solicitud.
- La tabla de sesiones ocupaba memoria y crecía con los clientes inactivos.
DELETEera opcional, y los clientes que se cerraban sin enviarlo dejaban entradas sin eliminar. - Los resultados de las listas podían variar según la conexión, por lo que no era seguro usar una caché delante del servidor.
Eliminar las sesiones elimina los cuatro problemas a la vez. Este es el cambio que conviene entender antes de modificar cualquier configuración.
Qué lleva ahora cada solicitud
Cada POST al endpoint MCP es independiente. La versión del protocolo y las capacidades del cliente se envían en el cuerpo de la solicitud dentro de _meta, y algunos campos se reflejan en cabeceras HTTP para que un intermediario pueda enrutar las solicitudes sin analizar JSON.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"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": {}
}
}
}io.modelcontextprotocol/protocolVersion y io.modelcontextprotocol/clientCapabilities son obligatorios en todas las solicitudes. clientInfo no es obligatorio, aunque los clientes deberían enviarlo. Una solicitud a la que le falte un campo obligatorio está mal formada, por lo que el servidor debe rechazarla con el error JSON-RPC -32602 y HTTP 400 Bad Request.
La cabecera Mcp-Method es obligatoria en todas las solicitudes. Mcp-Name es obligatoria en tools/call, resources/read y prompts/get. El valor de la cabecera debe coincidir con el cuerpo. Si el servidor procesa el cuerpo y detecta una discrepancia, debe rechazarla con 400 Bad Request y los códigos de error -32020 y HeaderMismatch. Esta regla existe porque un balanceador de carga que enruta según la cabecera y un servidor que ejecuta según el cuerpo usan dos fuentes de verdad distintas. Si enruta o limita la tasa según estas cabeceras, compruebe primero MCP-Protocol-Version: las revisiones anteriores nunca validaban la cabecera frente al cuerpo, por lo que en esas versiones el valor de la cabecera no es fiable.
La discrepancia de versiones es ahora un error normal de cada solicitud, no un handshake fallido. Un servidor que no implemente la versión solicitada responde 400 Bad Request con los errores -32022 y UnsupportedProtocolVersion, y enumera las versiones compatibles en data.supported. El cliente elige una de esa lista y vuelve a intentarlo.
Dónde fue el estado: tokens, cursores y suscripciones
El estado no desapareció. Se trasladó a lugares que se pueden ver y registrar.
Las credenciales pasan a cada solicitud. No hay una sesión a la que asociar una identidad, por lo que el token de acceso se envía en cada llamada HTTP y se valida cada vez. Los detalles se explican en la sección de autenticación siguiente.
Los cursores deben contener su propia posición. La paginación en tools/list, resources/list, prompts/list y resources/templates/list usa una cadena de cursor opaca, y los clientes no deben analizarla ni modificarla. En un servidor de un solo proceso, era habitual mantener el desplazamiento en memoria, asociado a la sesión. Sin sesión, el cursor debe ser suficiente para que cualquier réplica reanude el listado. Para ello, codifique la posición dentro del cursor y fírmelo, o guárdela en un almacenamiento compartido por todas las réplicas. Un cursor no válido debe devolver -32602. Fírmelo porque un cursor opaco sigue siendo una entrada proporcionada por el cliente que el código descodifica y considera fiable.
Las suscripciones pertenecen a una solicitud, no a una conexión. Un cliente que quiera recibir notificaciones de cambios envía subscriptions/listen con un filtro que indica los tipos que solicita: toolsListChanged, promptsListChanged, resourcesListChanged y resourceSubscriptions. El servidor responde con notifications/subscriptions/acknowledged y mantiene abierto ese flujo de respuesta. Si el flujo se interrumpe, el servidor no conserva nada y el cliente vuelve a enviar subscriptions/listen para recuperarlo.
El estado de la aplicación entre llamadas se convierte en un identificador explícito. Cuando un servidor realmente debe recordar algo entre llamadas, la respuesta de la especificación es un identificador generado por el servidor que se devuelve como un argumento de herramienta normal. Aparece en el esquema de la herramienta, se puede registrar y nunca está implícito en la conexión. Un servidor con datos reales por usuario, como un servidor de correo MCP autoalojado, usa este patrón en lugar de una sesión: el identificador del buzón o del borrador es un argumento de herramienta, por lo que cualquier réplica puede continuar con la siguiente llamada. Muchas herramientas no necesitan ningún identificador: una herramienta de búsqueda respaldada por su propia instancia de SearXNG recibe una consulta y devuelve los resultados, sin nada que reanudar en la siguiente llamada y sin motivo para depender de qué réplica respondió.
Despliegue: proxy inverso, tiempos de espera y comprobaciones de estado
El endpoint de MCP es una ruta que acepta POST. La mayor parte del tráfico consiste en una petición breve y una respuesta JSON, que cualquier proxy puede gestionar. La excepción es la respuesta en streaming, donde los valores predeterminados del proxy funcionan en su contra. Esta es la parte que cambia al pasar de una demostración en un portátil a un servidor MCP ejecutándose en un VPS.
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off es importante porque nginx almacena en búfer las respuestas del proxy de forma predeterminada. Esto retiene los eventos SSE hasta que el búfer se llena o termina la respuesta. La especificación también pide que los servidores envíen X-Accel-Buffering: no en las respuestas SSE, y nginx respeta esa cabecera. Por tanto, un servidor correcto indica por sí solo al proxy cómo debe comportarse. Configure también la directiva, porque esa es la parte que usted controla.
proxy_read_timeout tiene un valor predeterminado de 60 segundos. Un flujo subscriptions/listen que permanezca inactivo durante más tiempo se cierra en nginx, no en el servidor. Por eso, los registros muestran un proceso saludable, mientras el cliente muestra un flujo interrumpido. Aumente este valor sólo en la ubicación de MCP, no en todo el servidor. También se recomienda que los servidores envíen una línea de comentario SSE (una línea que comienza con dos puntos) como keep-alive durante los periodos de inactividad. Así se evita que los intermediarios agoten el tiempo de espera del flujo.
Caddy necesita menos configuración. De forma predeterminada, almacena parcialmente en búfer para mejorar la eficiencia en la red y vacía el búfer de inmediato cuando la respuesta incluye Content-Type: text/event-stream. Por tanto, el streaming funciona sin directivas adicionales.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Observe a qué apunta esa comprobación de estado. No dirija una comprobación activa al endpoint de MCP con GET, porque un servidor que sólo implementa esta revisión responde 405 Method Not Allowed a GET y DELETE, y el método de comprobación predeterminado de Caddy es GET. El proxy marcaría como inactivo un backend perfectamente saludable. Sirva una ruta sencilla como /healthz para el proxy y compruebe el protocolo por separado con un POST.
curl -sS https://mcp.example.com/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":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'Un 200 que contiene una lista supportedVersions indica que el proceso está activo y se comunica mediante el protocolo. Un 404 con el error JSON-RPC -32601 indica que el proceso está activo, pero no sirve server/discover, que todos los servidores 2026-07-28 deben implementar. Un 400 con -32022 indica que el comprobador solicitó una versión que esta compilación no admite. Esto es exactamente lo que debe detectar después de actualizar una dependencia. nginx de código abierto no incluye comprobaciones de estado activas. Por tanto, use max_fails y fail_timeout pasivos en el upstream y ejecute la comprobación del protocolo desde su sistema de monitorización.
Un reinicio gradual sólo hace que se pierdan las peticiones en curso. Drene el proceso, deje que terminen los POST abiertos, inicie el proceso nuevo y permita que los clientes vuelvan a emitir las peticiones que hayan fallado. Lo único que se sigue interrumpiendo es cualquier flujo subscriptions/listen abierto, porque ese flujo es una conexión activa con un proceso concreto. La ausencia de estado elimina la afinidad de sesión. No elimina la afinidad de conexión de un flujo que está abierto en ese momento, y ninguna regla de enrutamiento puede corregirlo. Un cliente puede distinguir ambos casos: un flujo que termina con el resultado subscriptions/listen vacío se cerró correctamente; un flujo que termina sin ese resultado se interrumpió, y el cliente puede interpretarlo como un motivo para reconectarse.
Ahora el almacenamiento en caché es posible por primera vez. Los resultados de los métodos de listado incluyen ttlMs y cacheScope, y cacheScope: "public" indica que los intermediarios compartidos pueden almacenar la respuesta en caché. Esto sólo es seguro porque los resultados de los listados ya no varían según la conexión, como consecuencia directa de eliminar las sesiones.
Por qué cambia la autenticación cuando no hay una sesión
Con una sesión, resultaba tentador autenticarse una vez en initialize y tratar después el ID de sesión como una prueba para todo. Usado de ese modo, un ID de sesión es una credencial de portador sin audiencia, caducidad ni mecanismo de revocación, emitida por su propio servidor. Al eliminar las sesiones, se elimina ese atajo y el mecanismo de sustitución es más estricto.
Un servidor MCP protegido actúa como un servidor de recursos OAuth 2.1. Cada solicitud HTTP del cliente debe incluir Authorization: Bearer <access token>, y el servidor valida el token en cada solicitud. La validación incluye la audiencia: el servidor debe confirmar que el token se emitió específicamente para él, conforme a RFC 8707 (Resource Indicators for OAuth 2.0), y no debe aceptar ni reenviar tokens destinados a otro servicio. Los clientes solicitan la audiencia correcta enviando el parámetro resource con el URI canónico del servidor.
El descubrimiento se inicia mediante un desafío. Cuando llega una solicitud sin un token utilizable, el servidor responde con 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"El cliente lee resource_metadata, obtiene ese documento (RFC 9728, OAuth 2.0 Protected Resource Metadata, que los servidores MCP deben implementar), identifica el servidor de autorización y ejecuta el flujo. Un token válido con permisos insuficientes recibe 403 Forbidden junto con error="insufficient_scope" y los ámbitos necesarios para esa operación.
Esto tiene dos consecuencias para la forma de ejecutarlo. La validación del token se realiza ahora en cada solicitud y no una vez por sesión, por lo que un recorrido de red hasta un endpoint de introspección en cada llamada afectará a la latencia: prefiera tokens que pueda verificar localmente mediante una firma, una audiencia y una caducidad, o almacene en caché el resultado de la validación durante un intervalo breve usando el token como clave. Además, como no existe una sesión que conserve una identidad, la autorización debe calcularse a partir del token en cada llamada. Esto es más transparente que el modelo de sesión y se combina con la práctica más amplia de mantener las credenciales fuera del proceso del agente, explicada en mantener secretos fuera de un agente de IA. Los ámbitos sólo limitan lo que puede hacer un token una vez que la solicitud llega al servidor; en la máquina donde se ejecuta el agente, los plugins del arnés que añaden reglas de permisos de herramientas y límites de presupuesto deciden qué llamadas se realizan.
Qué es cierto en esta revisión y qué no
Todo lo anterior describe la revisión 2026-07-28. No describe MCP para siempre ni describe el servidor que implementó el año pasado.
Los clientes y servidores que usan 2025-11-25 y anteriores todavía emplean el modelo de negociación inicial. La especificación denomina heredadas a esas revisiones y denomina modernas a las revisiones con metadatos por solicitud. Un servidor que sólo admita esta revisión, al comunicarse con un cliente antiguo, debe responder 405 Method Not Allowed a GET o DELETE en el endpoint de MCP, ignorar cualquier cabecera Mcp-Session-Id sin generar ni devolver ninguna, e ignorar Last-Event-ID porque los streams no se pueden reanudar. Un servidor compatible con ambas épocas puede ofrecer las dos en un mismo endpoint: una solicitud que incluya _meta se atiende sin estado, y una solicitud initialize selecciona la semántica de sesión anterior.
Por tanto, compruebe la cadena de revisión antes de dar por válido todo esto. Si su SDK todavía envía initialize, las sesiones siguen siendo reales en su implementación y usted todavía debe gestionar los problemas relacionados con sesiones descritos anteriormente. Lo mismo se aplica en el lado del cliente: un proceso de agente en su propio equipo, como el descrito en ejecutar un agente de programación en un VPS, sólo es sin estado en este sentido si la biblioteca que utiliza admite una revisión moderna. Consulte la versión que negocia su runtime y, después, la revisión correspondiente de la especificación. Considere esta página como la descripción de una revisión concreta, no del protocolo en general.
FAQ
¿Un servidor MCP sin estado significa que no puedo almacenar nada?
No. Sin estado describe el protocolo, no la aplicación. Las bases de datos, las colas y las cachés siguen funcionando exactamente igual. Lo que cambia es que el estado que abarca varias llamadas debe referenciarse mediante un identificador explícito que el cliente envía en cada solicitud, como un handle generado por el servidor en un argumento de herramienta. Lo que no puede hacer es inferir el contexto a partir de la conexión: la especificación indica que un servidor no debe depender de solicitudes anteriores realizadas sobre la misma conexión para establecer capacidades, versión del protocolo o identidad del cliente, porque cada solicitud proporciona esos datos en _meta.
¿Sigo necesitando sesiones persistentes en el balanceador de carga?
No para las solicitudes normales. En la revisión 2026-07-28, cada POST incluye su propia versión del protocolo, sus capacidades y sus credenciales. Por tanto, cualquier réplica puede responder a cualquier solicitud y el reparto round-robin es suficiente. Lo único que queda con una duración prolongada es el flujo de respuesta subscriptions/listen, que es una única conexión abierta con un solo proceso. Termina cuando termina ese proceso, y el cliente vuelve a enviar subscriptions/listen para restablecerlo. Eso corresponde a la duración de la conexión, no a la afinidad de sesión, y ninguna regla de enrutamiento lo impide.
¿Qué ocurrió con Mcp-Session-Id y el flujo HTTP GET?
Ambos se eliminaron en la revisión 2026-07-28, conforme a SEP-2567 y SEP-2575. Un servidor que implemente sólo esta revisión debe responder 405 Method Not Allowed a GET y DELETE en el endpoint de MCP, y debe ignorar una cabecera Mcp-Session-Id en lugar de devolverla. Las notificaciones de cambios iniciadas por el servidor ahora viajan en el flujo de respuesta de una solicitud subscriptions/listen, en lugar de utilizar un flujo GET independiente. Los servidores que deban seguir atendiendo a clientes antiguos implementan el comportamiento de la revisión anterior junto con el de esta revisión.
¿Cómo compruebo la salud de un servidor MCP sin handshake?
Use dos niveles. Configure la comprobación activa del proxy para que consulte una ruta HTTP normal que sirva la aplicación, porque un GET al endpoint de MCP devuelve correctamente 405 y marcaría como caído un backend saludable. Después, compruebe el protocolo mediante POST de server/discover. Todo servidor 2026-07-28 debe implementarlo. Verifique que la respuesta sea HTTP 200 y que incluya una versión del protocolo utilizada por sus clientes. Un 404 con un error JSON-RPC -32601 indica que el proceso está activo, pero no sirve ese método. Un 400 con -32022 indica que la versión solicitada no es compatible con esa compilación.