MCP 2026-07-28 hace stateless el protocolo: una petición POST a /mcp con MCP-Protocol-Version, Mcp-Method y Mcp-Name, sin initialize ni Mcp-Session-Id

MCP 2026-07-28: el protocolo se vuelve stateless y escala como cualquier API

MCP 2026-07-28 es la revisión más importante del Model Context Protocol desde su lanzamiento. Retira el handshake initialize/initialized y la cabecera Mcp-Session-Id, convierte cada petición en una operación autosuficiente y añade Multi Round-Trip Requests (MRTR) para que las confirmaciones del usuario ya no necesiten streams abiertos. En la práctica: puedes desplegar un servidor MCP detrás de un balanceador round-robin cualquiera, sin almacenamiento compartido ni sesiones pegajosas.

Respuesta rápida

  • Qué cambió: el núcleo de MCP pasa de protocolo bidireccional con estado a protocolo request/response stateless. Cada petición lleva su versión de protocolo, identidad y capacidades del cliente en _meta.
  • Qué se elimina: el intercambio initialize/initialized y la cabecera Mcp-Session-Id quedan oficialmente retirados (SEP-2575, SEP-2567).
  • Qué llega: MRTR para confirmaciones, routing por cabeceras Mcp-Method y Mcp-Name, listados con ttlMs y cacheScope, endurecimiento de autorización, framework formal de extensiones y una política de deprecación con 12 meses mínimos de margen.
  • Qué significa para ti: cualquier request puede caer en cualquier instancia detrás de un balanceador round-robin sin estado compartido. Tu servidor MCP se despliega como una API HTTP normal.
  • Costo: los cambios son rompientes. Si dependías de identificadores de sesión, la migración es real, aunque el SDK ayuda.
  • SDKs: TypeScript, Python, Go y C# ya hablan 2026-07-28. El SDK de Rust lo soporta en beta.

La cifra que sitúa la versión

Los mantenedores explican el contexto: los SDK de nivel 1 (Tier 1) acumulan cerca de 500 millones de descargas al mes, y los SDK de TypeScript y Python ya han superado cada uno los 1.000 millones de descargas acumuladas. Ese volumen es exactamente el que hace inviable el modelo de sesión: sticky sessions, almacenamiento compartido y afinidad de instancia no escalan a esa demanda.

Qué significa realmente “stateless” en MCP

Conviene ser preciso, porque aquí hay un malentendido muy común.

Lo que desaparece es el estado a nivel de protocolo: la sesión, el identificador, el ciclo de vida negociado y los streams bidireccionales que quedaban abiertos esperando que el servidor iniciara una petición hacia el cliente.

Lo que NO desaparece es tu aplicación. Si tu servidor necesita recordar algo entre llamadas, la guía oficial es explícita: minta un handle explícito desde una herramienta y pídele al modelo que lo devuelva como argumento. El razonamiento del equipo es interesante — dicen que esto funciona mejor que el estado escondido en el transporte porque el modelo puede ver el handle y pasarlo entre herramientas.

@mcp.tool()
def create_deployment(repo: str) -> dict:
    handle = deployments.create(repo)      # estado en tu app, no en el protocolo
    return {"deployment_id": handle, "instructions": "Pasa deployment_id al llamar a deploy_status"}

Es un cambio mental importante: el estado se mueve de la infraestructura hacia tu dominio.

Los siete cambios que importan

Cambio Detalle Impacto para desarrolladores
Sin handshake ni sesión Cada request es autosuficiente, con _meta Round-robin sin afinidad ni estado compartido
MRTR resultType: "input_required" + reintento con inputResponses Elicitation y sampling sin stream abierto
Routing por headers Mcp-Method y Mcp-Name obligatorios en Streamable HTTP Gateways, rate limiters y WAFs enrutan sin parsear JSON
Listados cacheables ttlMs y cacheScope en tools/list, prompts/list, resources/list, resources/read Catálogos cacheados y caches de prompt estables
Autorización endurecida RFC 9207 iss, application_type, credenciales ligadas al issuer Menos superficie de ataque OAuth
Extensions + Tasks Framework formal; Tasks sale del core experimental Trabajo largo y apps embebidas como extensión
Política de deprecación 12 meses mínimos de ventana Puedes planificar upgrades

1. Adiós al handshake: cada request viaja solo

Antes, un cliente tenía que completar initialize + initialized, recibir un Mcp-Session-Id y reenviarlo en cada petición posterior. Eso ataba al cliente a una instancia concreta.

Ahora cada request lleva su versión de protocolo y la identidad del cliente:

POST /mcp HTTP/1.1
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/clientInfo":{"name":"my-app","version":"1.0"}}}}

Si el cliente quiere conocer las capacidades del servidor por adelantado, existe el nuevo RPC server/discover — pero es opcional. Esa opcionalidad es la clave del diseño: puedes cachear capacidades una vez o no cachearlas en absoluto, y ambas opciones funcionan.

2. MRTR: confirmaciones sin streams abiertos

Este es el cambio más elegante desde el punto de vista de arquitectura.

El problema: para pedirle algo al usuario a mitad de una herramienta (una confirmación, un parámetro que falta), el servidor iniciaba elicitation/create sobre un stream que debía permanecer abierto.

La solución en 2026-07-28 (SEP-2322): el servidor devuelve un resultado con resultType: "input_required" junto con las peticiones que necesita respondidas. El cliente responde y reintenta la llamada original con las respuestas adjuntas en inputResponses.

{
  "resultType": "input_required",
  "requests": [
    { "method": "elicitation/create", "params": { "message": "¿Confirmas este despliegue en producción?" } }
  ]
}

Para un servidor stateless esto es transformador: la confirmación ya no es una conexión persistente, es un ciclo de dos requests HTTP. Cloudflare lo resume así: “permite que nuestras herramientas confirmen con el usuario antes de actuar, como el coste de un proyecto nuevo antes de crearlo, o una query que borraría datos”.

3. Routing por cabeceras: tu gateway ya no tiene que parsear JSON

Las peticiones de Streamable HTTP ahora deben incluir Mcp-Method y Mcp-Name (SEP-2243).

Esto es una mejora de operaciones, no de API. Tu balanceador, tu rate limiter o tu WAF pueden enrutar, autorizar y medir leyendo cabeceras, sin deserializar cuerpos JSON. Es el mismo modelo de las APIs REST de verdad: decides por método y recurso antes de tocar el payload.

4. Listados cacheables: el catálogo de herramientas deja de ser volátil

Las respuestas de tools/list, prompts/list, resources/list y resources/read ahora llevan ttlMs y cacheScope (SEP-2549).

El beneficio real tiene dos componentes:

  • El cliente puede cachear el catálogo de herramientas con un TTL que el servidor decide.
  • La respuesta incluye un orden determinista, lo que mantiene estables los caches de prompt del proveedor entre reconexiones.

Si tenías un gateway que reescribía o reordenaba tools/list para “limpiar” la salida, esa práctica ahora es contraproducente: rompe deliberadamente la estabilidad de cache que la especificación pide.

5. Autorización: cuatro cambios que cierran agujeros reales

La especificación dedicó más esfuerzo a autorización que a ninguna otra área, y los motivos son concreto:

  • iss según RFC 9207 (SEP-2468): los servidores de autorización deben devolver el parámetro iss y los clientes deben validarlo antes de canjear el código. Cierra una ventana de confusión entre servidores de autorización.
  • application_type en DCR (SEP-837): los clientes lo envían durante el registro dinámico para que los servidores dejen de rechazar redirecciones a localhost en apps de escritorio y CLI. Si alguna vez un servidor de autorización rechazó a tu cliente CLI con un error de redirect_uri, esta es probablemente la razón.
  • Credenciales ligadas al issuer (SEP-2352): las credenciales de cliente quedan vinculadas al servidor de autorización que las emitió. No hay reutilización entre servidores distintos.
  • DCR queda deprecado formalmente en favor de Client ID Metadata Documents (CIMD). DCR sigue funcionando por compatibilidad, pero se eliminará en una versión futura.

6. Extensions y Tasks: las extensiones fuera del core

Tasks deja el core experimental y se convierte en la extensión io.modelcontextprotocol/tasks, con consulta por polling mediante tasks/get y un nuevo tasks/update (SEP-2663).

Los cambios de notificaciones se mudan del antiguo endpoint HTTP GET a un único stream subscriptions/listen al que los clientes se suscriben por tipo de notificación.

Esto es arquitectura de plataforma a la vista: la spec sigue engordando, pero lo que es opcional se está sacando del núcleo y poniéndolo en extensiones. MCP Apps y Enterprise Managed Authorization (EMA) viven en el mismo framework. FastMCP 4.0 ya anunció soporte de primera clase para tasks de background, interactividad stateless y auth empresarial.

7. Deprecaciones con fecha: 12 meses de margen

Se deprecados Roots, Sampling y Logging (SEP-2577). Siguen funcionando y seguirán funcionando al menos 12 meses. Las implementaciones nuevas no deberían adoptarlos.

El transporte legacy HTTP+SSE queda oficialmente deprecado, con un año de margen.

El SDK de Python va más allá y deprecia también los métodos de suscripción a recursos del cliente (SEP-2575) y el progreso de cliente a servidor.

La existencia de una política formal de deprecación con ventana mínima de 12 meses es, en sí misma, un cambio importante para cualquier equipo que ponga MCP en producción.

Qué está pasando en el ecosistema

El anuncio incluye respaldos de AWS (que ya ofrece el núcleo stateless en Amazon Bedrock AgentCore), Cloudflare (soporte desde el día cero en Workers), Microsoft Foundry, Google Cloud, Netlify, Figma, Anthropic, Supabase, Xero, Honeycomb y Runlayer.

Si todavía no has conectado un servidor MCP a tu asistente de IA, aquí ya te lo explicamos paso a paso: PulsAI conecta tus datos de Garmin con ChatGPT, Claude y Codex mediante MCP y Garmin MCP: conecta tu Garmin a la IA de forma gratuita con Codex, OpenCode y cualquier cliente MCP. Ambos montajes siguen siendo válidos bajo el protocolo nuevo y son el mejor banco de pruebas para ver qué cambia en tu setup.

Dos datos concretos que dan contexto de adopción:

  • En Honeycomb.io, casi el 20% de todas las consultas interactivas mensuales las hacen agentes.
  • Manufact (que usa el nuevo SDK v2 en su framework mcp-use) reporta haber reducido el tamaño del paquete alrededor del 83% y hacerlo 25% más rápido, gracias al nuevo split cliente-servidor.

David Soria Parra, maintainer principal, lo resume así: “Es un salto en cómo se sirven servidores MCP escalables, y recoge todas las lecciones aprendidas en 18 meses para dar una base sólida al futuro de MCP.”

Nick Cooper, del equipo central de MCP, lo puso en términos de diseño web: “MCP tiene ya aproximadamente un año y medio de vida. Gracias a los comentarios de los desarrolladores y del resto de la comunidad, está evolucionando hacia un protocolo más maduro que incorpora lecciones de décadas de diseño de protocolos web.”

Cómo migrar tu servidor Python a MCP v2

El SDK de Python es el que trae la guía de migración más detallada. Si tu servidor usa FastMCP, esto es lo que te va a romper.

Orden de migración recomendado

La guía oficial propone este orden, y conviene respetarlo:

  1. Actualiza los pines de dependencias y el uso de la CLI.
  2. Aplica los renombrados mecánicos y los movimientos de import.
  3. Migra la superficie del servidor (MCPServer o Server de bajo nivel).
  4. Migra el código de cliente.
  5. Actualiza transportes y autenticación.
  6. Ejecuta los tests y revisa la validación más estricta del protocolo.
  7. Resuelve los avisos de deprecación.

Cambios que golpean a casi todos los proyectos

Cambio v1 → v2 Síntoma que verás
FastMCP → MCPServer ModuleNotFoundError: No module named 'mcp.server.fastmcp'
camelCase → snake_case AttributeError: 'Tool' object has no attribute 'inputSchema'
mcp.types reduce nombres ImportError: cannot import name 'Content' from 'mcp.types'
McpError → MCPError ImportError: cannot import name 'McpError' from 'mcp'
URIs de recurso son str AttributeError: 'str' object has no attribute 'host'
Uniones sin RootModel AttributeError: 'LoggingMessageNotification' object has no attribute 'root'
streamablehttp_client eliminado ImportError: cannot import name 'streamablehttp_client'
httpx → httpx2 ModuleNotFoundError: No module named 'httpx'
Client con mode='auto' logs del servidor con server/discover inesperado
Parámetros de transporte fuera del constructor TypeError: __init__() got an unexpected keyword argument 'port'
Handlers sync en worker thread asyncio.get_running_loop() falla en un handler def

Antes y después: el caso típico

Servidor (v1):

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("mi-servidor")

@mcp.tool()
def sumar(a: int, b: int) -> int:
    return a + b

if __name__ == "__main__":
    mcp.run(port=8000)

Servidor (v2):

from mcp import MCPServer

mcp = MCPServer("mi-servidor")

@mcp.tool()
def sumar(a: int, b: int) -> int:
    return a + b

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8000)

Tres cosas cambiaron: el nombre de la clase, el nombre por defecto del servidor (de FastMCP a mcp-server) y la ubicación de port, que ahora va a run() y no al constructor.

El cambio de mayúsculas que más confunde

Los atributos Python pasaron de camelCase a snake_case. El formato JSON en el cable no cambia: los alias de Pydantic siguen emitiendo camelCase. Lo que cambia es tu acceso a atributos, y tu model_dump() necesita by_alias=True si quieres producir JSON.

# v1
result = await session.call_tool("mi_tool", {"x": 1})
if result.isError:
    ...
tools = await session.list_tools()
cursor = tools.nextCursor
schema = tools.tools[0].inputSchema

# v2
result = await session.call_tool("mi_tool", {"x": 1})
if result.is_error:
    ...
tools = await session.list_tools()
cursor = tools.next_cursor
schema = tools.tools[0].input_schema

Renombres comunes: inputSchema → input_schema, outputSchema → output_schema, isError → is_error, nextCursor → next_cursor, mimeType → mime_type, structuredContent → structured_content, serverInfo → server_info, protocolVersion → protocol_version, uriTemplate → uri_template, listChanged → list_changed.

httpx2: el cambio silencioso

El SDK ahora depende de httpx2 en lugar de httpx + httpx-sse. Es API-compatible, así que en la mayoría de casos solo cambia el import. Pero hay un fallo que no da ningún error:

Si tu código hace except httpx.ConnectError: y httpx sigue instalado por otra dependencia, ese bloque sigue importando bien y simplemente nunca vuelve a coincidir. El SDK lanza excepciones httpx2, no httpx. Audita los except httpx. y los isinstance junto con los imports.

Además, httpx2 valida certificados contra el almacén de confianza del sistema operativo vía truststore, no contra el bundle de certifi. En contenedores mínimos sin CA del sistema, apunta SSL_CERT_FILE o SSL_CERT_DIR a un bundle, o construye un ssl.SSLContext explícito.

El split cliente-servidor

El cambio de mayor impacto en el SDK Python es arquitectónico: ClientSession ya no es una BaseSession, corre sobre un JSONRPCDispatcher distinto al servidor, y BaseSession se eliminó. Además, Server.run() ya no acepta un flag stateless: la dirección stateless es ahora la esperada.

OpenTelemetry viene activado por defecto: cada request saliente lleva un envelope _meta para propagación de trazas. Por eso opentelemetry-api pasa a ser dependencia obligatoria.

Server de bajo nivel: los decoradores desaparecieron

Si usabas el Server de bajo nivel con decoradores, v2 los reemplaza por parámetros on_* en el constructor:

# v1
server = Server("mi-servidor")

@server.list_tools()
async def handle_list_tools():
    return [Tool(name="sumar", ...)]

# v2
async def handle_list_tools():
    return [Tool(name="sumar", ...)]

server = Server("mi-servidor", on_list_tools=handle_list_tools)

En este nivel los cambios son más profundos: el wrapping automático de valores de retorno desaparece (una list o dict sueltos ahora fallan la validación en lugar de envolverse), y las excepciones en handlers de herramientas ya no se convierten en CallToolResult(is_error=True) sino en errores JSON-RPC reales. El constructor pasa a ser solo con keyword arguments.

Checklist de migración

  • ☐ Actualizar pines: mcp>=2,<3, sse-starlette>=3, httpx2>=2.5.0.
  • ☐ httpx → httpx2 en imports, except y fixtures de test.
  • ☐ FastMCP → MCPServer.
  • ☐ Mover parámetros de transporte del constructor a run()/app().
  • ☐ Convertir atributos de tipos a snake_case.
  • ☐ Revisar model_dump() y añadir by_alias=True donde emitas JSON.
  • ☐ Sustituir McpError por MCPError.
  • ☐ Tratar los URIs de recurso como str.
  • ☐ Asumir que los handlers def corren en un worker thread: usa async def si necesitas el event loop.
  • ☐ Si usabas sesiones para estado, migra a handles explícitos.
  • ☐ Verifica que tu gateway o WAF enrutan por Mcp-Method y Mcp-Name.
  • ☐ Añade iss validation si gestionas autorización.
  • ☐ Elimina Roots, Sampling y Logging de implementaciones nuevas.
  • ☐ Revisa timeouts: ahora son float en segundos, no timedelta, y el error es -32001, no 408.
  • ☐ Comprueba el límite de 4 MiB en cuerpos de peticiones Streamable HTTP.

No tienes prisa. La rama de mantenimiento v1.x sigue recibiendo correcciones de bugs críticos y parches de seguridad, y la recomendación oficial es mantener un límite superior <2 en mcp hasta que termines la migración.

Preguntas frecuentes

¿MCP 2026-07-28 rompe la compatibilidad con clientes y servidores antiguos?

Los cambios son rompientes, pero los cuatro SDK de nivel 1 hablan ya la versión nueva y la ventana de deprecación da 12 meses para lo que queda marcado como deprecado. Lo que se retira (handshake, sesiones) requiere actualización coordinada de cliente y servidor.

¿Sigo necesitando sesiones si mi servidor tiene estado?

No del lado del protocolo. El estado vive en tu aplicación. La práctica recomendada es emitir un handle explícito desde una herramienta y que el modelo lo devuelva como argumento en llamadas posteriores.

¿Qué es MRTR exactamente?

Multi Round-Trip Requests. Permite que un servidor pida información al usuario a mitad de una herramienta sin mantener un stream abierto: devuelve resultType: "input_required" con las peticiones necesarias y el cliente reintenta la llamada original con inputResponses.

¿Puedo desplegar mi servidor MCP en cualquier hosting?

Sí, esa es la idea central. Al ser un protocolo request/response stateless sobre HTTP, funciona en cualquier plataforma que sirva HTTP, sin sesiones pegajosas ni almacenamiento compartido. Cloudflare, Netlify, AWS y Microsoft lo confirman en el anuncio oficial.

¿Qué pasa con el transporte HTTP+SSE?

Queda oficialmente deprecado con un año de margen. Streamable HTTP es el camino.

¿Migrar el SDK Python v2 es urgente?

No. La rama v1.x sigue con soporte de correcciones críticas y parches de seguridad. Lo razonable es planificar la migración, pero no hay urgencia forzada por el SDK.

¿MCP Tasks sustituye al WebSocket?

No exactamente. Tasks es una extensión para trabajo de larga duración con consulta por polling (tasks/get, tasks/update). El transporte WebSocket fue eliminado del SDK de Python, y las notificaciones viajan por streams subscriptions/listen.

¿Siguen funcionando Roots, Sampling y Logging?

Sí, durante al menos 12 meses más, pero están deprecados. Si estás construyendo desde cero, no los adoptes.

Fuentes

Artículos relacionados en Hackeruna


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *