Inicio / Artículos

Ejecute las herramientas MCP como servicios sin estado después de la especificación del 2026-07-28.

Eliminar sesiones pegajosas: MCP 2026-07-28 hace que las solicitudes sean autónomas mediante el enrutamiento de encabezados, reintentos con input_required y claves requestState compartidas entre réplicas.

Editor: REACTAPP.TOP8 min de lectura1843 palabras

Fuentes: MCP Just Went Stateless: Run Your Agent Tools Like Real Services

Por qué las sesiones representaban un problema en producción

Operar MCP detrás de un balanceador de carga solía requerir la afinidad de sesiones, asignando a cada cliente a un único pod y aceptando que las actualizaciones habituales pudieran interrumpir a los agentes en medio de una tarea. La revisión MCP 2026–07–28 elimina por completo las sesiones a nivel de protocolo. Los mantenedores la describieron como el cambio más significativo desde la implementación de la autorización, y para los equipos que operan herramientas de agentes a gran escala es el cambio más importante.

La guía a continuación explica este cambio y luego crea un pequeño servidor de reembolsos sin estado utilizando el SDK oficial de Python v2, incluyendo un paso de aprobación humana que sobrevive a los saltos entre réplicas. Las muestras se probaron con mcp==2.2.0.

MCP comenzó como un puente entre la computadora de escritorio y las herramientas locales a través de stdio, donde era aceptable utilizar un socket con estado que permaneciera activo durante mucho tiempo. En Kubernetes, Cloud Run y otras plataformas de múltiples instancias, ese enfoque se volvió costoso:

  • Ruteo pegajoso. El proceso de establecimiento de conexión mediante initialize generaba un Mcp-Session-Id, y las llamadas posteriores tenían que dirigirse al mismo proceso.
  • Despliegues frágiles. Reiniciar un pod eliminaba todas las sesiones activas en él.
  • Gateways opacos. Los límites de tasa y el ruteo en una WAF o gateway API requerían analizar los cuerpos de mensajes JSON-RPC.
  • Flujos manteniéndose abiertos. Cuando una herramienta necesitaba una respuesta humana durante una llamada, el servidor mantenía el flujo abierto a la espera.

Esa estructura basada en estado era adecuada para asistentes de programación locales. Las empresas, en cambio, necesitaban un acceso normal a producción con políticas centralizadas.

Cambios en las especificaciones implementados el 2026-07-28

1. Los identificadores de handshake y sesión ya no existen. Los pasos emparejados initialize / initialized y el encabezado Mcp-Session-Id han desaparecido. Cada solicitud incluye la versión del protocolo, la identidad del cliente y sus capacidades dentro de params._meta. Cualquier réplica bajo un balanceo de carga simple por round-robin puede aceptar cualquier llamada.

2. El enrutamiento se encuentra en los encabezados HTTP. El tráfico HTTP transmisible por secuencias debe contener Mcp-Method y Mcp-Name. Las pasarelas pueden enrutar, medir y bloquear las llamadas a las herramientas sin necesidad de abrir el cuerpo de la solicitud.

3. Solicitudes de ida y vuelta múltiples (MRTR). En lugar de mantener abierto un flujo en espera de una respuesta humana, el servidor responde con resultType: "input_required", los prompts y un blob opaco de requestState. El cliente recopila las respuestas y vuelve a intentar la llamada original con inputResponses. Este patrón reemplaza las llamadas en curso enviadas por el servidor como elicitation/create, sampling/createMessage y roots/list.

4. Listas cachebillables. Las respuestas para tools/list, prompts/list, resources/list y resources/read pueden indicar ttlMs y cacheScope, de modo que los clientes dejen de hacer solicitudes repetidas a los endpoints de listas y los cachés de prompts del LLM permanezcan más estables.

5. Autorización más estricta. Se requieren verificaciones por parte del emisor según RFC 9207. El registro dinámico de clientes da paso a los documentos de metadatos del ID del cliente.

6. Extensiones. Las tareas de larga duración dejan el núcleo experimental para la extensión io.modelcontextprotocol/tasks. Las aplicaciones MCP y la autorización gestionada empresarial también se convierten en extensiones oficiales.

7. Obsoletización con plazo de transición. Roots, Sampling, el RPC de registro y HTTP+SSE heredado permanecerán disponibles al menos doce meses mientras los equipos realizan la migración.

No se garantiza que los servidores nuevos funcionen con clientes antiguos. Actualice ambos lados simultáneamente cuando tenga control sobre ellos. Las implementaciones personalizadas del protocolo enfrentan mejoras significativas en comparación con los usuarios de SDK.

En práctica: un servidor MCP sin estado en Python

Crea un servicio mínimo de reembolsos con dos herramientas. get_order busca un pedido. refund_order es delicado, por lo que se requiere la aprobación de un humano antes de transferir el dinero. Esa pregunta hecha durante la llamada es precisamente lo que antes exigía un alto grado de afinidad entre las partes.

Instala el SDK v2 y crea un secreto de sellado compartido:

pip install "mcp[cli]>=2.2.0"
export MCP_STATE_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")

El servidor

# server.py — a stateless MCP server (spec 2026-07-28, Python SDK v2)
import os
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.caching import CacheHint
from mcp.server.mcpserver import Elicit, RequestStateSecurity, Resolve

# Tell clients they may cache tools/list for 1 hour and share it across users.
mcp = MCPServer(
    "refunds",
    cache_hints={"tools/list": CacheHint(ttl_ms=3_600_000, scope="public")},
    # Every replica must share this key, or a retry that lands on another pod fails.
    request_state_security=RequestStateSecurity(keys=[os.environ["MCP_STATE_KEY"]]),
)

ORDERS = {"A-1001": 49.99, "A-1002": 1299.00}  # stand-in for your real database


class Approval(BaseModel):
    approve: bool
    reason: str = ""


def ask_for_approval(order_id: str) -> Elicit[Approval]:
    """Resolver: ask a human to confirm the refund before the tool runs."""
    amount = ORDERS[order_id]
    return Elicit(f"Refund ${amount:,.2f} for order {order_id}?", Approval)


@mcp.tool()
def get_order(order_id: str) -> dict:
    """Look up an order's refundable amount."""
    return {"order_id": order_id, "amount": ORDERS.get(order_id)}


@mcp.tool()
def refund_order(
    order_id: str,
    approval: Annotated[Approval, Resolve(ask_for_approval)],
) -> str:
    """Refund an order after a human approves it."""
    if not approval.approve:
        return f"Refund for {order_id} rejected: {approval.reason or 'no reason given'}"
    return f"Refunded ${ORDERS[order_id]:,.2f} for {order_id}"


if __name__ == "__main__":
    # No session state: any replica behind a round-robin load balancer can answer.
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000,
            stateless_http=True, json_response=True)

Tres detalles son importantes:

  • Resolve junto con Elicit constituyen el estilo de inyección de dependencias de v2. El parámetro approval no es llenado por el modelo; el SDK ejecuta primero ask_for_approval y, para los clientes de 2026-07-28, convierte la solicitud en un resultado input_required. Los clientes más antiguos recurren al método clásico de solicitud durante la llamada.
  • cache_hints agrega ttlMs / cacheScope a tools/list.
  • stateless_http=True junto con json_response=True genera una respuesta JSON por cada POST: no hay objeto de sesión ni flujo en memoria.
  • El cliente

    # client.py — call the server; the SDK handles the input_required retry loop
    import asyncio
    
    from mcp import Client
    from mcp.types import ElicitResult
    
    
    async def on_elicit(context, params):
        # In a real app, render params.message in your UI and collect the answer.
        print(f"[server asks] {params.message}")
        return ElicitResult(action="accept", content={"approve": True})
    
    
    async def main():
        async with Client("http://localhost:8000/mcp", elicitation_callback=on_elicit) as client:
            tools = await client.list_tools()
            print("tools:", [t.name for t in tools.tools], "| ttlMs:", tools.ttl_ms)
    
            result = await client.call_tool("refund_order", {"order_id": "A-1002"})
            print(result.content[0].text)
    
    
    asyncio.run(main())
    

    Una ejecución típica muestra algo similar a:

    tools: ['get_order', 'refund_order'] | ttlMs: 3600000
    [server asks] Refund $1,299.00 for order A-1002?
    Refunded $1,299.00 for A-1002
    

    Qué se transmite realmente

    La primera llamada realizada con curl muestra los encabezados de enrutamiento y el sobre _meta que reemplazan al antiguo proceso de establecimiento de conexión:

    curl -s -X POST http://localhost:8000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H 'MCP-Protocol-Version: 2026-07-28' \
      -H 'Mcp-Method: tools/call' \
      -H 'Mcp-Name: refund_order' \
      -d '{
        "jsonrpc": "2.0", "id": 1, "method": "tools/call",
        "params": {
          "name": "refund_order",
          "arguments": {"order_id": "A-1002"},
          "_meta": {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"},
            "io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}}}
          }
        }
      }'
    

    El servidor responde de inmediato en lugar de bloquearse:

    {
      "resultType": "input_required",
      "inputRequests": {
        "__main__:ask_for_approval": {
          "method": "elicitation/create",
          "params": {
            "message": "Refund $1,299.00 for order A-1002?",
            "mode": "form",
            "requestedSchema": {
              "type": "object",
              "properties": {"approve": {"type": "boolean"}, "reason": {"type": "string"}},
              "required": ["approve"]
            }
          }
        }
      },
      "requestState": "v1.bOKJzdeMdGCz-ezv..."
    }
    

    Después de que una persona responde, el cliente vuelve a intentar la misma llamada tools/call y agrega las respuestas junto con el bloque de estado opaco:

    "inputResponses": {
      "__main__:ask_for_approval": {"action": "accept", "content": {"approve": true}}
    },
    "requestState": "v1.bOKJzdeMdGCz-ezv..."
    

    La respuesta de seguimiento devuelve resultType: “complete” con texto como Se reembolsaron $1,299.00 por A-1002. En esas dos solicitudes POST, ningún proceso guardó en memoria local la información de la conversación; el campo opaco requestState contenía todos los datos necesarios para continuar.

    El problema en producción: las claves requestState

    Para demostrarlo, cree réplicas en 8000 y 8001, dirija la solicitud inicial a A y la de seguimiento a B. Si los métodos de cifrado difieren, el segundo paso rechazará el bloque de estado:

    {"code": -32602, "message": "Invalid or expired requestState",
     "data": {"reason": "invalid_request_state"}}
    

    Ese fallo es intencional. El sellado utiliza AES-256-GCM dentro del SDK para que quienes lo usan no puedan falsificar ni editar el blob. Cada proceso genera una clave privada a menos que se configure lo contrario, lo que significa que la réplica B no puede descifrar un blob sellado en A. Configure RequestStateSecurity(keys=[...]) y cargue un secreto de al menos 32 bytes desde su gestor de secretos en cada réplica. Con una clave compartida, el intento de reintentar entre réplicas tiene éxito.

    El SDK también admite la rotación sin interrupciones. La primera clave de la lista sella el nuevo estado; todas las claves de la lista pueden abrirlo. Envíe primero una lista con dos claves, con el secreto antiguo y luego el de reemplazo; cambie el orden en la siguiente actualización y elimine la entrada antigua una vez que haya transcurrido un período completo de TTL (600 segundos a menos que lo modifique). Prefiera codec= cuando desee utilizar su propio KMS en lugar de claves en bruto.

    Lista de verificación para producción

    • Ejecuta con stateless_http=True y elimina las reglas de sesiones persistentes del balanceador de carga.
    • Comparte la clave requestState entre las réplicas y planifica su rotación.
    • Mantén el estado empresarial duradero en tu propio almacén. Las tareas largas deben devolver un identificador que el modelo pueda utilizar posteriormente, o utiliza la extensión Tasks; no dependas de la conexión para almacenar datos en memoria.
    • Gestiona las operaciones en el perímetro con Mcp-Method / Mcp-Name para establecer límites, listas de permisos y medición por herramienta.
    • Publica indicaciones para el caché: utiliza TTL largos para listas de herramientas estables, y scope="private" cuando los resultados varían según el usuario.
    • Deja intactos los RPC obsoletos: reemplaza el registro de logs por OpenTelemetry o stderr, y programa las salidas de Sampling y Roots dentro del plazo de doce meses.
    • Actualiza el sistema de autenticación: valida a los emisores (RFC 9207) y migra hacia Documentos de Metadatos de ID de Cliente.
  • Mejore los clientes y servidores juntos; no se garantiza la compatibilidad hacia atrás.
  • Puntos clave

    • La revisión del 28-07-2026 hace que cada solicitud sea autónoma; los códigos de handshake y las IDs de sesión ya no existen.
    • Las preguntas durante la llamada utilizan input_required junto con una repetición por parte del cliente para que los servidores nunca dejen un flujo abierto sin manejarlo.
    • El SDK de Python v2 lleva la mayor parte de la carga: Resolve(...) junto con Elicit(...) genera la aprobación humana necesaria, funcional tanto para clientes antiguos como nuevos.
    • Las repeticiones entre réplicas requieren una clave requestState compartida antes de escalar la infraestructura.
    • El enrutamiento mediante encabezados y las indicaciones de caché permiten que MCP funcione como servicios HTTP normales.

    Conclusión

    MCP ya no es solo un protocolo de complementos para escritorio. Puede ejecutarse como cualquier otro microservicio, lo que significa que vuelven a ser aplicables la escalabilidad automática, los despliegues blue/green, las pasarelas API y las WAF. Si ya opera servidores MCP, pase al SDK v2, establezca una clave de estado compartido y elimine las sesiones pegajosas; quienes estén de guardia notarán la diferencia.

    Leer más

    • Anuncio de la especificación: blog.modelcontextprotocol.io
    • Texto del protocolo: modelcontextprotocol.io/specification/2026-07-28
    • Artículo de la industria en The Register sobre cómo MCP abandona su diseño basado en estado
    • Repositorio oficial del SDK para Python

    Leer relacionado

  • MCP Desde Cero, Parte 3: Conectar un Cliente a tu Servidor — Construir un cliente MCP stdio que inicie hr_server.py, llame a search_employee y devuelva resultados, sin tener que iniciar el servidor manualmente.
  • Conectar Herramientas MCP a través de LiteLLM: Guía paso a paso del gateway bancario simulado — Registrar un banco simulado mediante streamable-HTTP, comparar los ciclos de herramientas desarrollados manualmente con una sola solicitud al gateway y demostrar que las medidas de protección pre_call nunca llegan hasta MCP.
  • ¿Qué es MCP? Conectar aplicaciones de IA a herramientas y datos — Conceptos básicos del Protocolo de Contexto de Modelos: hosts, clientes, servidores, descubrimiento de herramientas, ejemplo con Neo4j, flujos agentes y límites de seguridad.
  • Introducción a MCP: de la fragmentación a los servidores de herramientas compartidos — Por qué falla el pegado de contexto, cómo ayudaron las llamadas a funciones, por qué perjudican las integraciones N×M, y cómo MCP te lleva hacia servidores reutilizables de tipo N+M.