Danila (Dayfing)
Volver a publicaciones
2173 palabras10 min

MCP 2026-07-28: servidores stateless y migración segura

La revisión de Model Context Protocol del 28 de julio de 2026 cambia MCP remoto. El núcleo ahora es stateless, sin estado de sesión, y usa solicitudes independientes. Cada solicitud lleva lo necesario para procesarla, así que un balanceador puede enviar la siguiente llamada a otra instancia. Añade server/discover, MRTR, cabeceras obligatorias, pistas de caché, extensiones y autorización reforzada. Los detalles normativos proceden del anuncio oficial y del registro de cambios.

Esto cambia el estado del protocolo, no obliga a que todo el negocio sea stateless. Una herramienta todavía puede usar una base de datos o un workflow duradero. Desaparece el estado oculto ligado a una sesión de transporte MCP.

Cambios frente al protocolo de 2025

El ciclo de vida anterior comenzaba con initialize, seguido de notifications/initialized. En Streamable HTTP, el servidor podía entregar Mcp-Session-Id y asociar los mensajes posteriores a esa conexión. En 2026-07-28 se eliminan el intercambio initialize y la cabecera de sesión del protocolo. Cada solicitud declara la versión y las capacidades del cliente en _meta. El cliente debería incluir io.modelcontextprotocol/clientInfo y el servidor debería identificarse en los metadatos del resultado.

Esta comparación sirve para planificar:

Área Comportamiento de 2025 Comportamiento de 2026-07-28
Ciclo de vida initialize y notifications/initialized No hay handshake del protocolo
Sesión Mcp-Session-Id HTTP opcional No hay sesión a nivel de protocolo
Capacidades Se negocian una vez Se declaran por solicitud
Descubrimiento Después de initialize o por convención El servidor moderno debe ofrecer server/discover, el cliente puede llamarlo
Servidor a cliente Solicitudes en un canal abierto MRTR devuelve solicitudes dentro de la respuesta
Enrutamiento HTTP El gateway suele leer el JSON Cabeceras Mcp-Method y Mcp-Name cuando corresponda
Listas y lecturas Frescura definida por el cliente Pistas ttlMs y cacheScope
Reanudación SSE podía usar IDs de eventos No hay reanudación Last-Event-ID, hay que repetir la solicitud
Registro DCR era el camino automático habitual CIMD es preferible, DCR queda por compatibilidad

La revisión mueve Tasks a la extensión io.modelcontextprotocol/tasks, sustituye el flujo antiguo por subscriptions/listen y declara obsoletos Roots, Sampling, Logging y HTTP+SSE. La política prevé al menos doce meses, pero las implementaciones nuevas no deberían adoptarlas.

Qué significa stateless en la práctica

Con Streamable HTTP moderno, el servidor expone un único endpoint MCP que acepta POST. El cliente envía una solicitud o notificación JSON-RPC por POST. La respuesta es un objeto JSON o un flujo SSE limitado a esa solicitud. El servidor no crea un identificador de sesión y un flujo interrumpido no conserva un historial para reanudar. En HTTP, cerrar el flujo de respuesta es la señal de cancelación.

Las cabeceras y el cuerpo describen la misma operación. Una llamada mínima a una herramienta es:

POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"q":"otters"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"catalog-app","version":"1.0.0"}}}}

El valor de MCP-Protocol-Version debe coincidir con _meta. Un servidor moderno rechaza una cabecera obligatoria ausente o incoherente con HTTP 400 y el código HeaderMismatch, -32020. Debe volver a validar las cabeceras después de que actúe el gateway. Así se evita que el proxy enrute por un nombre de herramienta y la aplicación ejecute otro.

El modelo stateless cambia el escalado, no el significado del negocio. Si un workflow necesita continuidad, devuelve un handle explícito y exige que aparezca en la llamada siguiente. Guarda el estado de autoridad en una base de datos o servicio de workflows, vincula el handle al usuario y a la operación y aplica una caducidad. Un handle, requestState o argumento sin verificar nunca demuestra permiso.

server/discover y compatibilidad de versiones

Todo servidor moderno debe implementar el RPC server/discover. Su resultado anuncia versiones y capacidades compatibles, además de instrucciones opcionales. El cliente puede llamarlo primero para elegir una versión o enviar directamente una solicitud moderna. Por tanto, discovery es útil, pero no es un handshake obligatorio para el cliente.

Si la versión solicitada no está disponible, el servidor devuelve UnsupportedProtocolVersionError con sus versiones. El cliente escoge una versión común y reintenta. Un cliente que admite ambas épocas debe clasificar bien la sonda. Un 400 vacío o sin un error JSON-RPC moderno reconocido puede indicar un endpoint antiguo. Una respuesta moderna reconocida exige corregir la solicitud o negociar de nuevo. Los errores de autorización y de infraestructura no demuestran que el servidor sea antiguo.

El servidor puede conservar una ruta legacy mientras los clientes nuevos usan solicitudes modernas. No deduzcas la época solo por una conexión TCP o un 404 genérico.

MRTR sustituye las solicitudes del servidor al cliente

El formato moderno elimina el canal de solicitudes JSON-RPC del servidor al cliente. Una herramienta que necesita confirmación, un dato faltante o un paso asistido por el modelo devuelve un resultado intermedio en lugar de mantener abierto el flujo. El resultado tiene resultType: "input_required" y un mapa inputRequests. El cliente responde y repite el método original con inputResponses. El reintento es una solicitud nueva y puede llegar a otra réplica.

Un flujo de confirmación puede verse así:

{
  "resultType": "input_required",
  "inputRequests": {
    "confirm": {
      "type": "elicitation",
      "message": "Delete three files?",
      "schema": {"type": "boolean"}
    }
  },
  "requestState": "signed-opaque-state"
}

Después, el cliente envía inputResponses.confirm y devuelve requestState byte por byte. El servidor debe volver a entrar en el handler como en una solicitud nueva. Haz el handler idempotente, deriva el paso actual del estado verificado y pide solo los datos que aún falten. No marques una acción destructiva como terminada antes de validar la confirmación.

requestState no es un contenedor seguro. Pasa por el cliente y debe tratarse como entrada controlada por un atacante. Fírmalo con HMAC o usa cifrado autenticado, vincúlalo al principal, al método original, a los parámetros relevantes y a una caducidad, y rechaza la manipulación antes de ejecutar el handler. Una firma no oculta el contenido, así que no guardes secretos allí. El SDK de TypeScript ofrece un codec de estado y un hook de verificación. Su legacy shim puede convertir el mismo handler input_required en las solicitudes antiguas elicitation/create, sampling/createMessage y roots/list mientras existan clientes de 2025.

Para trabajos largos usa Tasks con un handle duradero, tasks/get y tasks/update.

Pistas de caché y catálogos deterministas

Los resultados modernos de tools/list, prompts/list, resources/list, resources/templates/list y resources/read incluyen ttlMs y cacheScope. ttlMs es una pista de frescura no negativa en milisegundos, parecida a max-age de HTTP. Cero significa que el resultado caduca de inmediato. En servidores antiguos, la ausencia debe tratarse como cero. Un valor positivo indica cuánto puede esperar el cliente antes de leer de nuevo, pero no garantiza que los datos no cambien. Comprueba la frescura cuando necesites los datos y no conviertas TTL en polling continuo.

La clave de caché debe incluir el método y cada parámetro que afecte al resultado, incluido el URI de un recurso y el cursor de una lista paginada. No guardes una respuesta con inputResponses o requestState, porque su contexto no está en una clave de lista simple. cacheScope: "public" permite compartir entre contextos de autorización. Úsalo solo para datos sin campos específicos del usuario o de permisos. La autorización por herramienta sigue siendo necesaria aunque el catálogo esté en caché.

El servidor debería devolver las herramientas en orden determinista. El orden estable mantiene los prompts y mejora el caché tras reconectar. listChanged complementa TTL, pero no lo sustituye.

OAuth y seguridad

La autorización es opcional en MCP. Un servidor HTTP que protege recursos debe seguir el perfil OAuth 2.1 de la especificación de 2026. Actúa como resource server y debe publicar OAuth Protected Resource Metadata según RFC 9728. Una respuesta 401 debería señalarla mediante WWW-Authenticate y un challenge de scopes cuando sea útil. El cliente debe admitir la URL del header y las dos formas well-known.

Los metadatos pueden identificar varios authorization servers. Los clientes deben admitir OAuth Authorization Server Metadata de RFC 8414 y OpenID Connect Discovery.

El registro de clientes prioriza Client ID Metadata Documents. El pre-registro también es válido. Dynamic Client Registration queda como alternativa obsoleta. Si se usa DCR, envía el application_type correcto para un cliente de escritorio o CLI. Valida PKCE, usa S256, registra redirect URI exactas y usa HTTPS, excepto un callback localhost permitido.

El cliente debe guardar el issuer validado junto con la transacción PKCE. Si la respuesta contiene iss, compáralo antes de canjear el código. Las credenciales quedan ligadas a su issuer y no deben reutilizarse con otro authorization server. Incluye el URI canónico del servidor MCP como resource RFC 8707 en las solicitudes de autorización y de token. El servidor debe validar la audiencia y rechazar tokens destinados a otro recurso.

En Streamable HTTP valida Origin para bloquear DNS rebinding. Un servidor local debería escuchar solo en localhost. No pongas bearer tokens en query strings ni logs, pide consentimiento antes de exponer recursos privados o llamar herramientas y trata las descripciones y anotaciones como no confiables si no confías en el servidor. 401 indica autorización ausente o inválida. 403 indica permisos insuficientes y debería incluir insufficient_scope cuando sea posible.

Migración del SDK de TypeScript

El SDK de TypeScript v2 separa los paquetes de cliente, servidor, core y runtime. Consulta la guía del SDK para 2026-07-28 y la guía de v1 a v2. Actualizar el SDK no hace que automáticamente salgan bytes modernos por la red. El cliente v2 negocia legacy por defecto, así que activa versionNegotiation de forma explícita.

Para un cliente que admite ambas épocas, la forma documentada es:

import { Client } from '@modelcontextprotocol/client';

const client = new Client(
  { name: 'catalog-app', version: '1.0.0' },
  { versionNegotiation: { mode: 'auto' } },
);
await client.connect(transport);

mode: 'auto' prueba server/discover y vuelve al handshake de 2025 solo con un peer realmente legacy. Fija 2026-07-28 si el fallback ocultaría una incompatibilidad. createMcpHandler(factory) crea un servidor HTTP moderno por solicitud y puede servir ambas épocas. Para seleccionar la época en stdio usa serveStdio(() => buildServer()).

Cambia el registro de handlers v1 basado en esquemas a cadenas de método, por ejemplo setRequestHandler('tools/call', handler). Sustituye ctx.sessionId por handles de aplicación o requestState verificado. Cambia la elicitation push por inputRequired(...). Una solicitud moderna no emite notificaciones de log sin io.modelcontextprotocol/logLevel. El codemod del SDK es ayuda mecánica, no una prueba de compatibilidad.

Para validar, ejecuta createMcpHandler mediante fetch y conserva cobertura del handshake legacy. Comprueba cabeceras, _meta, idempotencia, caché, audiencia y estado HTTP con clientes antiguos y modernos.

Lista de migración

  1. Inventaria clientes, servidores, transportes, almacenes de sesión, event stores SSE y lecturas de Mcp-Session-Id.
  2. Decide qué endpoint recibe tráfico moderno y cuál mantiene temporalmente el legacy.
  3. Actualiza el SDK y fija las versiones reales en el lockfile.
  4. Añade server/discover y una política de negociación de versiones.
  5. Haz que cada solicitud sea autónoma y valida _meta y las cabeceras reflejadas.
  6. Sustituye el estado por sesión por handles o requestState firmado y con caducidad.
  7. Reescribe las interacciones del servidor con MRTR y haz seguros los reintentos.
  8. Añade ttlMs, cacheScope y orden determinista a catálogos y recursos.
  9. Actualiza gateways, WAF, métricas y trazas para Mcp-Method y Mcp-Name.
  10. Implementa comprobaciones de issuer, resource, audience, PKCE, redirect, Origin y scopes.
  11. No adoptes HTTP+SSE, Roots, Sampling, Logging ni DCR en código nuevo.
  12. Despliega gradualmente con observabilidad, compara errores modernos y legacy y retira la compatibilidad después de migrar a los consumidores.

Solución de problemas

Síntoma Causa probable Acción
HTTP 400 con -32020 Falta una cabecera o no coincide con el cuerpo Recalcula MCP-Protocol-Version, Mcp-Method y Mcp-Name desde el mismo objeto
HTTP 400 y error de versión El peer no sirve la revisión solicitada Elige una versión de supported o la ruta legacy
HTTP 404 y method-not-found El endpoint es moderno, pero el método no existe Comprueba método y extensión, no inicies initialize a ciegas
HTTP 401 o 403 durante discovery La sonda está bloqueada por autenticación Corrige credenciales y metadatos, el estado auth no prueba legacy
No hay notificaciones de log Falta io.modelcontextprotocol/logLevel Actívalo por solicitud o usa stderr y OpenTelemetry
Efectos secundarios duplicados El flujo ya no se puede reanudar Usa idempotency keys y un ID de solicitud nuevo
Datos de un usuario en otro caché El resultado se marcó public por error Usa private y conserva controles del principal
Cliente antiguo recibe 405 en GET Espera HTTP+SSE Mantén temporalmente una ruta legacy

Para el contexto de arquitectura, consulta production AI agent architecture y MCP server with TypeScript and OAuth.

Fuentes

Esta guía sigue la especificación MCP 2026-07-28, su registro de cambios y funciones obsoletas, los requisitos de Streamable HTTP, SEP-2575 sobre MCP sin estado, el anuncio de la versión 2026-07-28, la especificación de autorización MCP y la documentación de migración del SDK de TypeScript.

Más publicaciones