Servidores MCP sin estado: qué cambió realmente
La revisión 2026-07-28 eliminó las sesiones y el handshake initialize de MCP. Revisa el impacto en proxy inverso, health checks, timeouts y autenticación.
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 responderla. Por tanto, cualquier proceso en cualquier máquina puede responder a cualquier solicitud. MCP (Model Context Protocol, el formato de comunicación que utilizan los agentes para acceder a herramientas) estableció esta regla en la revisión 2026-07-28. Esa revisión eliminó el intercambio de mensajes initialize y la sesión HTTP subyacente.
Ese es el objetivo operativo. Un servidor que no conserva información por cliente puede situarse detrás de un balanceador de carga convencional 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 componentes 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 debe inferirse ningún estado de las solicitudes anteriores, ni siquiera cuando utilizan la misma conexión o el mismo flujo.
Sin estado no significa que el servidor no almacene nada. La base de datos, la cola y la caché siguen existiendo. Significa que el protocolo no transporta estado en la conexión. Por tanto, el servidor no debe tratar una conexión, un proceso o un socket abierto como equivalente a «este cliente, en mitad de una conversación».
Qué eliminó la revisión 2026-07-28
2026-07-28 es la revisión actual de la especificación a 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
GETindependiente en el que los servidores enviaban notificaciones. Se sustituye porsubscriptions/listen, un POST normal cuya respuesta es un flujo de larga duración. - La posibilidad de reanudar los flujos SSE (eventos enviados por el servidor). La cabecera
Last-Event-IDy los identificadores por evento desaparecen. Por tanto, si se interrumpe un flujo, se pierde la solicitud en curso y el cliente debe volver a enviarla como una solicitud nueva con un identificador de solicitud nuevo. ping,logging/setLevelynotifications/roots/list_changed. El nivel de registro ahora es un campo por solicitud,io.modelcontextprotocol/logLevelen_meta.
Se añadió un método, que todos los servidores deben implementar. server/discover devuelve en una sola llamada las versiones de protocolo, las capacidades y la identidad que admite el servidor. Es lo más parecido a un handshake que queda, y los clientes no están obligados a llamarlo.
Por qué el transporte con sesiones era difícil de ejecutar 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 durante InitializeResult. Después, el cliente tenía que 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 no válido y que el cliente comenzara de nuevo con unInitializeRequestnuevo. Cada despliegue se convertía en un evento de reconexión para 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 pendientes. - Los resultados de las listas podían variar según la conexión, por lo que el almacenamiento en caché delante del servidor no era seguro.
Eliminar las sesiones resuelve los cuatro problemas a la vez. Este es el cambio que conviene entender antes de modificar cualquier configuración.
Qué transporta ahora cada solicitud
Cada solicitud POST al endpoint de 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. Algunos campos también se copian en cabeceras HTTP para que un intermediario pueda enrutar las solicitudes sin analizar el 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 tanto, 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, HeaderMismatch. Esta regla existe porque un balanceador de carga que enruta según la cabecera y un servidor que ejecuta según el cuerpo utilizan 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 contra el 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 una solicitud, en lugar de un handshake fallido. Un servidor que no implemente la versión solicitada responde con 400 Bad Request, junto con los errores -32022 y UnsupportedProtocolVersion, y enumera las versiones que admite en data.supported. El cliente selecciona una de esa lista y vuelve a intentarlo.
Dónde quedó el estado: tokens, cursores, suscripciones
El estado no desapareció. Pasó a lugares que puede consultar y registrar.
Las credenciales pasan a cada solicitud. No existe una sesión a la que asociar una identidad, por lo que el token de acceso acompaña cada llamada HTTP y se valida cada vez. Consulte los detalles en la sección de autenticación siguiente.
Los cursores deben llevar 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 contener información 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 su código decodifica y en la que confía.
Las suscripciones pertenecen a una solicitud, no a una conexión. Un cliente que quiere recibir notificaciones de cambios envía subscriptions/listen con un filtro que especifica los tipos que quiere: 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 especificación propone 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 queda implícito por la conexión. Un servidor con datos reales por usuario detrás, como un servidor de correo MCP autohospedado, 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 procesar la siguiente llamada.
Despliegue: reverse proxy, 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 solicitud corta y una respuesta JSON, que cualquier proxy puede gestionar. La excepción es la respuesta en streaming, donde los valores predeterminados del proxy juegan en su contra. Esta es la parte que cambia cuando pasa 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 reenviadas de forma predeterminada. Esto retiene los eventos SSE hasta que se llena un búfer o termina la respuesta. La especificación también solicita 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í mismo 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 mediante nginx, no mediante el servidor. Por eso los registros muestran un proceso en buen estado, mientras el cliente muestra un flujo interrumpido. Auméntelo 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 requiere menos configuración. De forma predeterminada, almacena parcialmente las respuestas en búfer para mejorar la eficiencia de transmisión y las vacía de inmediato cuando la respuesta contiene 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, mientras que el método de comprobación de estado predeterminado de Caddy es GET. El proxy marcaría como caído un backend perfectamente saludable. Sirva una ruta sencilla como /healthz para el proxy y compruebe el protocolo por separado mediante 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 utiliza el protocolo. Un 404 con el error JSON-RPC -32601 indica que el proceso está activo, pero no proporciona 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 ahora sólo interrumpe las solicitudes en curso. Drene el servicio, deje que terminen los POST abiertos, inicie el proceso nuevo y permita que los clientes vuelvan a emitir las solicitudes que hayan fallado. Lo único que seguirá interrumpiéndose 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. El cliente puede distinguir ambos casos: un flujo que termina con el resultado subscriptions/listen vacío se cerró correctamente; uno que termina sin ese resultado se interrumpió, y el cliente puede tratarlo como un motivo para reconectar.
El almacenamiento en caché pasa a ser posible por primera vez. Los resultados de los métodos de lista ahora 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 las listas 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, era tentador autenticarse una vez en initialize y tratar después el ID de sesión como una prueba para todo. Un ID de sesión usado de esa forma es una credencial de tipo bearer sin audiencia, caducidad ni mecanismo de revocación, emitida por su propio servidor. Eliminar las sesiones elimina ese atajo y obliga a aplicar un reemplazo 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 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 scopes necesarios para esa operación.
Esto tiene dos consecuencias para la operación. La validación del token ahora se realiza en cada solicitud y no una sola vez por sesión. Por tanto, un recorrido de red hasta un endpoint de introspección en cada llamada afectará a la latencia. Es preferible usar tokens que se puedan verificar localmente mediante una firma, una audiencia y una caducidad, o almacenar en caché el resultado de la validación durante un intervalo corto, identificado por el token. Además, como no hay una sesión que mantenga la identidad, la autorización debe calcularse a partir del token en cada llamada. Este modelo es más transparente que el modelo basado en sesiones y encaja con la práctica más amplia de mantener las credenciales fuera del proceso del agente, que se explica en mantener los secretos fuera de un agente de IA.
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 de 2025-11-25 y anteriores todavía usan el modelo de negociación inicial. La especificación denomina heredadas esas revisiones y denomina modernas 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 eras puede atender 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 antigua.
Por tanto, compruebe la cadena de revisión antes de dar por válido cualquiera de estos puntos. Si su SDK todavía envía initialize, las sesiones siguen siendo reales en su implementación y los problemas relacionados con sesiones descritos anteriormente siguen siendo responsabilidad suya. Lo mismo se aplica al lado del cliente: un proceso de agente en su propio equipo, como el de la configuración descrita 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 entorno de ejecución, lea después la revisión correspondiente de la especificación y trate 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 abarque varias llamadas debe referenciarse mediante un identificador explícito que el cliente envíe en cada solicitud, como un identificador 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 en la misma conexión para establecer capacidades, versión del protocolo o identidad del cliente, porque cada solicitud incluye 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, capacidades y credenciales, por lo que cualquier réplica puede responder a cualquier solicitud y el round-robin es suficiente. Lo único de larga duración que queda es el flujo de respuesta subscriptions/listen, que es una única conexión abierta con un único proceso. Termina cuando termina ese proceso, y el cliente vuelve a enviar subscriptions/listen para restablecerlo. Esto 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 por 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 el estado de un servidor MCP sin handshake?
Use dos niveles. Configure la comprobación activa del proxy contra 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 que está funcionando. Después, compruebe el protocolo mediante POST de server/discover, que todo servidor 2026-07-28 debe implementar, y verifique que la respuesta sea HTTP 200 y que incluya una versión del protocolo que utilicen sus clientes. Un 404 con el error JSON-RPC -32601 indica que el proceso está ejecutándose, pero no sirve ese método. Un 400 con -32022 indica que esa compilación no admite la versión solicitada.