Saltar al contenido
← Cuaderno

Arquitectura de Agentes & MCP

Stateless MCP y Tasks Extension: Escalando Agentes de IA en Producción sin Sticky Sessions

Cómo la nueva especificación del Model Context Protocol elimina las sesiones ligadas a servidores, resuelve el cuello de botella en balanceadores de carga y permite ejecutar herramientas pesadas con tareas duraderas.

Fecha
8 oct 2026
Lectura
8 min
Resultado
0 Sticky Sessions • Enrutamiento Transparente por Headers • Cero Conexiones Caídas • Coste $0 Local

En los últimos meses de 2026, el Model Context Protocol (MCP) se ha consolidado como el estándar de facto para conectar modelos de lenguaje con herramientas externas y fuentes de datos. Con más de 240 millones de descargas mensuales en sus SDKs y decenas de miles de servidores desarrollados por la comunidad y grandes tecnológicas, el protocolo ha demostrado su potencia conceptual.

Sin embargo, en cuanto un equipo de ingeniería intenta pasar un prototipo de MCP a producción distribuida, suele estrellarse contra un muro invisible: la trampa de la conexión con estado (Stateful Transport).

El diseño original del protocolo dependía de un intercambio de inicialización obligatorio (initialize), identificadores de sesión fijos (Mcp-Session-Id) y conexiones abiertas persistentes vía HTTP+SSE. En un entorno real con Kubernetes, contenedores Docker y balanceadores de carga estándar, este diseño causaba estragos:

  1. Las peticiones consecutivas enviadas a través de un balanceador round-robin terminaban en pods distintos, arrojando errores inmediatos de Session Not Found.
  2. Forzar afinidad de sesión (sticky sessions) concentraba el tráfico en nodos específicos, rompiendo el autoescalado y saturando la infraestructura.
  3. Las herramientas lentas (scrapers, generación de informes, procesamiento ETL o compilación de código) mantenían sockets HTTP abiertos durante minutos, provocando timeouts en cascada y fugas de descriptores de archivos.

La revisión arquitectónica del protocolo (especificación finalizada bajo la tutela de la Agentic AI Foundation de la Linux Foundation) ataca de raíz estos tres problemas. En este artículo de trinchera en YesWeLab (@Yeswelab_OU) analizamos cómo funciona la nueva arquitectura Stateless MCP, cómo implementar la extensión Tasks para operaciones asíncronas de larga duración y cómo articular nuestra estrategia Dual Track para operar agentes a escala.


1. De la Trampa de las Sticky Sessions a la Petición Autocontenida

En las versiones iniciales de MCP, la comunicación entre el cliente del agente y el servidor remoto simulaba una sesión de terminal fija:

[ ARQUITECTURA LEGACY: DEPENDIENTE DE SESIÓN ]
Cliente Agente ──────► Balanceador (Round-Robin)
                            │
               ┌────────────┴────────────┐
               ▼ (Req 1: initialize)     ▼ (Req 2: tools/call)
          [ Pod MCP A ]             [ Pod MCP B ]
      (Registra session_id: 123)   (ERROR: Session 123 no existe)

Para solucionar esto, los equipos configuraban sticky sessions (afinidad por cookie o IP de cliente). Esto creaba un cuello de botella grave: si un agente intensivo ejecutaba cientos de llamadas de herramientas, un solo pod absorbía toda la carga mientras los demás servidores permanecían ociosos. Además, hacía imposible desplegar servidores MCP en plataformas serverless (AWS Lambda, Cloudflare Workers) o entornos de borde (edge).

La Solución: Protocolo Completamente Sin Estado (Stateless Wire Protocol)

La nueva especificación elimina por completo el handshake initialize previo y los identificadores Mcp-Session-Id. Cada petición HTTP entrante es 100% autocontenida.

[ ARQUITECTURA STATELESS: CUALQUIER POD RESPONDE ]
Cliente Agente ──────► Balanceador (Nginx / Traefik / Envoy)
                            │ (Round-Robin puro)
               ┌────────────┼────────────┐
               ▼            ▼            ▼
          [ Pod MCP 1 ] [ Pod MCP 2 ] [ Pod MCP 3 ]
        (Procesa Req) (Procesa Req) (Procesa Req)

Para hacer esto viable sin perder eficiencia, el protocolo introduce dos mecanismos clave a nivel de transporte:

  1. Headers HTTP estándar para inspección y enrutado:
    • Mcp-Method: Especifica el método JSON-RPC (por ejemplo, tools/call o tools/list).
    • Mcp-Name: Especifica la herramienta o recurso concreto (por ejemplo, ejecutar_consulta_sql). Esto permite que API Gateways, proxies inversos y balanceadores puedan aplicar rate limiting, cuotas por usuario o enrutado basado en rutas sin tener que deserializar el cuerpo JSON de la petición.
  2. Caché determinista de catálogo de herramientas: Las llamadas a tools/list incorporan cabeceras de tiempo de vida (TTL) y ámbitos de caché. Los clientes no necesitan consultar el catálogo en cada reconexión, reduciendo la latencia de inicio y preservando los bloques cacheados en el prompt del LLM (prompt cache).

2. Tasks Extension: Desacoplando Herramientas de Larga Duración

Uno de los mayores dolores de cabeza al construir agentes autónomos ocurre cuando una herramienta tarda más de un par de segundos en responder:

  • Un agente solicita clonar un repositorio y analizar 2.000 archivos.
  • Un pipeline en n8n lanza una ingesta pesada de documentos OCR.
  • Un generador de vídeo o renderizado 3D procesa un clip complejo.

Mantener la conexión HTTP abierta mientras el servidor trabaja es extremadamente frágil: proxies intermedios cortan conexiones por inactividad a los 60 segundos, reinicios de red causan pérdidas de estado y los workers del servidor se bloquean esperando operaciones de I/O.

El Patrón Asíncrono de Tasks

La extensión oficial Tasks convierte las herramientas de larga ejecución en operaciones asíncronas no bloqueantes:

[ FLUJO ASÍNCRONO CON TASKS EXTENSION ]

Cliente Agente                       Servidor MCP (Stateless)
      │                                         │
      ├─── tools/call (scraping_complejo) ─────►│
      │                                         │ [Inicia job en background]
      │◄── 202 Accepted (task_handle: t_948a) ──┤
      │                                         │
      │    [El agente no cuelga el socket]      │
      │                                         │
      ├─── tasks/get (task_handle: t_948a) ────►│
      │◄── Status: "running", progress: 65% ────┤
      │                                         │
      ├─── tasks/get (task_handle: t_948a) ────►│
      │◄── Status: "completed", data: [...] ────┤

El estado de la tarea no reside en la conexión HTTP ni en la memoria del hilo de ejecución: vive en un almacén persistente compartido detrás de un identificador duradero (task_handle). Cualquier réplica del servidor MCP puede responder a la comprobación de estado tasks/get.


3. Implementación de Trinchera: Servidor FastMCP con Tareas Asíncronas y SQLite WAL

A continuación se muestra un patrón de implementación real en Python utilizando FastMCP desacoplado, donde las tareas pesadas se persisten en una base de datos local SQLite con modo WAL (Write-Ahead Logging), garantizando cero colisiones y compatibilidad total con balanceadores round-robin.

import sqlite3
import uuid
import threading
import time
from typing import Dict, Any
from mcp.server.fastmcp import FastMCP

# Instancia del servidor MCP Stateless
mcp = FastMCP("lab-data-tasks-service")

DB_PATH = "/tmp/mcp_tasks.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("PRAGMA journal_mode=WAL;")
    conn.execute("""
    CREATE TABLE IF NOT EXISTS tasks (
        id TEXT PRIMARY KEY,
        tool_name TEXT,
        status TEXT, -- 'queued', 'running', 'completed', 'failed'
        progress INTEGER,
        result TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    """)
    conn.commit()
    conn.close()

init_db()

def worker_ejecucion_pesada(task_id: str, query: str):
    """Simula una tarea intensiva en segundo plano."""
    conn = sqlite3.connect(DB_PATH)
    conn.execute("UPDATE tasks SET status='running', progress=20 WHERE id=?", (task_id,))
    conn.commit()
    
    # Procesamiento real (ej. extracción, indexación, scraping)
    time.sleep(3)
    
    resultado = f"Dataset procesado con éxito para: {query}"
    conn.execute("""
        UPDATE tasks 
        SET status='completed', progress=100, result=? 
        WHERE id=?
    """, (resultado, task_id))
    conn.commit()
    conn.close()

@mcp.tool(name="iniciar_analisis_dataset")
def iniciar_analisis_dataset(query: str) -> Dict[str, Any]:
    """Inicia una tarea pesada y devuelve un task_handle inmediato."""
    task_id = f"task_{uuid.uuid4().hex[:10]}"
    
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        INSERT INTO tasks (id, tool_name, status, progress) 
        VALUES (?, 'iniciar_analisis_dataset', 'queued', 0)
    """, (task_id,))
    conn.commit()
    conn.close()
    
    # Disparar ejecución desacoplada
    threading.Thread(target=worker_ejecucion_pesada, args=(task_id, query), daemon=True).start()
    
    return {
        "status": "accepted",
        "task_handle": task_id,
        "poll_endpoint": f"tasks/get?id={task_id}",
        "message": "Operación iniciada. Comprueba el estado con el task_handle."
    }

@mcp.tool(name="consultar_estado_tarea")
def consultar_estado_tarea(task_handle: str) -> Dict[str, Any]:
    """Consulta el progreso o resultado de una tarea durable."""
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("SELECT status, progress, result FROM tasks WHERE id=?", (task_handle,))
    row = cur.fetchone()
    conn.close()
    
    if not row:
        return {"error": "Tarea no encontrada"}
        
    return {
        "task_handle": task_handle,
        "status": row[0],
        "progress_percent": row[1],
        "result": row[2] if row[0] == "completed" else None
    }

4. Filosofía Dual Track en YesWeLab

En YesWeLab aplicamos el principio “Show, Don’t Sell”: probamos arquitecturas reales en combate antes de recomendarlas a la comunidad. Frente a este nuevo estándar, estructuramos dos caminos según las necesidades del proyecto:

Track A: DIY / Open Source / Auto-alojado (Coste $0)

  • Stack: Servidores FastMCP (Python o Node.js) empaquetados en Docker Compose + Reverse Proxy Traefik o Nginx con balanceo round-robin.
  • Persistencia de Tareas: SQLite en modo WAL montado en volumen persistente o Redis local (valkey).
  • Ventajas: Cero dependencias de proveedores externos, privacidad total de los datos de las herramientas, sin costes recurrentes por conexión o licencias de API Gateway.

Track B: Fast-Track SaaS / Cloud Enterprise

  • Stack: Despliegue de endpoints MCP en Cloudflare Workers o AWS API Gateway + Redis Serverless (Upstash) + Autenticación federada OIDC (Okta / Microsoft Entra ID).
  • Escalabilidad: Cero mantenimiento de servidores, tolerancia a picos masivos de llamadas multi-agente y cumplimiento de seguridad con la extensión Enterprise-Managed Authorization.
  • Ventajas: Despliegue global en cuestión de minutos, alta disponibilidad garantizada y control centralizado de acceso a las herramientas empresariales.

5. Checklist de Trinchera para Arquitectos de Agentes

Antes de desplegar un servidor MCP en producción bajo alta demanda, verifica estos puntos clave:

  1. [ ] Cero estado a nivel de transporte: Asegúrate de que tus servidores no guardan variables de sesión en memoria local que dependan de peticiones previas del cliente.
  2. [ ] Elimina afinidad de sesión en balanceadores: Configura tu Nginx, Traefik o Ingress Controller con algoritmo round-robin o least connections estándar; no configures sticky sessions.
  3. [ ] Aislamiento de tareas de larga duración: Toda herramienta que exceda los 5 segundos debe devolver un task_handle duradero y ejecutarse en segundo plano (Tasks pattern).
  4. [ ] Modo WAL en persistencia local: Si usas SQLite para registrar tareas o estados intermedios, activa siempre PRAGMA journal_mode=WAL; para evitar bloqueos de concurrencia entre hilos y workers.
  5. [ ] Enrutado seguro por headers: Aprovecha los headers Mcp-Method y Mcp-Name en tu API Gateway para aplicar políticas de limitación de tasa específicas en las herramientas más costosas.
#Model Context Protocol SDK#FastMCP#Docker#Traefik#SQLite WAL#Redis#n8n v1.88+#Python