Como tema candente en nuestro mundo, pensar en agente de IA nos lleva a pensar en un sistema inteligente, automatizaciones pero… ¿lo has pensado a nivel de infraestructura y concurrencia interna?

Con los MCP, los LLMs pueden conectarse a nuestras BBDD, APIs, herramientas internas… pero cada llamada externa al mundo es latencia y latencias. Si a esta ecuación le añadimos los Virtual Threads de Java, esta orquestación de agentes empieza a buscar una forma eficiente para no saturar nuestros servidores.

En este post iremos desde la integración del MCP dentro de Java con Spring AI, pasando por los hilos virtuales y su forma de salvarnos la vida cuando miles de agentes se ejecutan de manera paralela. Os doy la bienvenida al mundo de la IA escalable con Java.

Por qué usar Virtual Threads con MCP

Querer (y necesitar) ir a la alta concurrencia en IA

Debemos saber qué pasa cuando un LLM utiliza una herramienta mediante un MCP, ya que el cómputo masivo de CPU es verdad que lo abstraemos a los propios modelos, pero sí que estamos ante un problema de I/O puro y duro. El agente suele estar esperando el 99% del tiempo: esperando que el LLM decida qué herramienta utilizar, esperando a la propia red o esperando al MCP para que le devuelva los datos del sistema.

Imaginemos esto pero elevado a miles de usuarios concurrentes usando pools de hilos tradicionales. El resultado también lo podemos imaginar: servidor sin memoria antes de que el LLM acabe de escribir su primer token. Por lo tanto, si realmente necesitas desplegar agentes de IA en producción, es cuando tienes que dar el salto a los hilos virtuales.

Una vez decidido, ¿por qué este combo?

El Model Context Protocol (MCP) es el nuevo estándar abierto para conectar clientes de IA con servidores de datos y herramientas. Al integrar Spring AI con soporte MCP sobre una JVM con hilos virtuales activos, obtenemos ventajas competitivas brutales:

Conceptos clave: Host, Server y Virtual Threads

La arquitectura de MCP combinada con Java moderno gira en torno a tres elementos:

Teniendo claro qué es cada uno, ¿cómo configuramos este ecosistema? Te lo mostramos con un ejemplo práctico.

Preparar nuestro proyecto Java para IA y MCP

Una vez decidido que vamos a montar agentes de alta disponibilidad, iremos paso a paso configurando nuestro entorno con Spring Boot y Spring AI.
Primero, generamos nuestro proyecto (si no lo tenemos ya) utilizando, por ejemplo, Spring Initializr, asegurándonos de utilizar una versión de Java 21 o superior.

En nuestro build.grade (o pom.xml si utilizamos maven), añadimos las dependencias necesarias para Spring AI y el starter de soporte para MCP:

plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.6'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
}

ext {
    set('springAiVersion', "2.0.0-M4")
}

dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-mcp-client'

    implementation 'org.springframework.ai:spring-ai-starter-model-ollama'

    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

Para activar la magia de la que hemos hablado para la concurrencia masiva, debemos irnos a nuestro nuestro archivo de configuración application.properties y añadir la siguiente línea, que le indicará a Spring Boot que delegue ese trabajo pesado a los hilos virtuales:

spring.threads.virtual.enabled=true

Una vez configurado, vamos a crear un servicio de IA muy básico. Este componente actuará como MCP Host, conectándose a un servidor MCP local (por ejemplo, un script de Node o Python que consulta información corporativa) y resuelve las peticiones de los usuarios:

package com.example.ai.mcp;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class AgentService {

    private final ChatClient chatClient;

    // Spring AI autoconfigura el cliente MCP y expone sus herramientas como beans de tipo ToolCallback.
    public AgentService(ChatClient.Builder chatClientBuilder, List<ToolCallback> mcpTools) {
        this.chatClient = chatClientBuilder
                .defaultTools((Object) mcpTools.toArray(new ToolCallback[0])) // Vincula las herramientas del servidor MCP
                .build();
    }

    public String askAgent(final String userPrompt) {
        return this.chatClient.prompt()
                .user(userPrompt)
                .call()
                .content();
    }
}

En este código, el ChatClient recupera dinámicamente las herramientas (getTools()) que el servidor MCP tiene disponibles. Cuando invocamos askAgent, el LLM evalúa el prompt del usuario y, si necesita datos externos, ejecuta de manera transparente la herramienta del servidor MCP a través de una llamada de red bloqueante.

Gracias a que activamos los hilos virtuales, mientras la red resuelve esa petición de herramientas, el hilo queda completamente libre para procesar otras interacciones de IA.

El "Efecto Matrioska" del bloqueo en agentes de IA

En una aplicación web de toda la vida, por ejemplo, una API que consulta una base de datos, el hilo atiende la petición HTTP, bloquea mientras lee de la BD, recibe el dato y responde. Hay un único bloqueo de I/O.

En un agente de IA con un MCP externo, se produce lo que me gusta llamar el bloqueo en Matrioska, que basicamente es un bloqueo dentro de otro bloqueo. Cuando invocamos nuestro método askAgent, pasa lo siguiente en un único hilo:

  1. Primer bloqueo (Envío al LLM): el cliente de Spring AI envía el prompt del usuario al LLM. El hilo se bloquea esperando la respuesta de la red.
  2. El LLM decide: el modelo analiza el texto y dice: "No tengo este dato, necesito ejecutar la herramienta consultarFactura". Y no, no busques ese método en el código anterior, esa es la magia, el MCP le indica las tools que tiene disponibles y él es capaz de decidir que necesita esa.
  3. Segundo bloqueo (llamada al MCP Server): Spring AI intercepta esa decisión del LLM y, a través del MCP, realiza una petición de red al servidor MCP externo donde reside la herramienta corporativa. El hilo se vuelve a bloquear esperando al servidor MCP.
  4. Tercer bloqueo (vuelta al LLM): el servidor MCP responde, Spring AI recoge el JSON, y se lo vuelve a enviar al LLM para que procese la respuesta final. El hilo se bloquea por tercera vez.

Si utilizamos hilos de la plataforma tradicionales (java.lang.Thread nativos del S.O.), un solo usuario consultando al agente mantendría secuestrado un hilo físico durante varios segundos a través de múltiples llamadas de red anidadas.

Con los Virtual Threads, la JVM realiza magia: cada vez que se inicia uno de estos 3 bloqueos de red, el hilo virtual cede el hilo físico (Carrier Thread) instantáneamente. El hilo físico pasa a atender a otros usuarios, y el hilo virtual "vuelve a la vida" en cualquier otro hilo físico libre solo cuando los bytes de la red están listos.

“Lo creo pero no lo veo”. Destripando los hilos en los logs

No hay nada mejor que una demostración para ver si todo esto que os estoy contando es cierto. Para ello, vamos a modificar el servicio que hemos creado para imprimir el hilo actual antes de llamar al ChatClient, y así poder ver la concurrencia:

 package com.example.ai.mcp;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class AgentService {

    private final ChatClient chatClient;

    // Usamos ObjectProvider para que el cliente MCP sea opcional y no rompa al arrancar en local
    public AgentService(ChatClient.Builder chatClientBuilder, ObjectProvider<SyncMcpToolCallbackProvider> mcpToolCallbackProvider) {

        mcpToolCallbackProvider.ifAvailable(provider -> chatClientBuilder.defaultToolCallbacks(provider.getToolCallbacks()));

        this.chatClient = chatClientBuilder.build();
    }

    public String askAgent(final String userPrompt) {
        System.out.println("DEBUG HILOS -> " + Thread.currentThread());

        return this.chatClient.prompt()
                .user(userPrompt)
                .call()
                .content();
    }
}

Al levantar nuestra aplicación Spring Boot 4 y lanzar un par de peticiones, la consola nos arrojará algo como esto:

¿Cómo se lee esta traza?

Si tuviéramos activadas las trazas del cliente MCP de Spring AI, veríamos que durante la llamada al servidor de herramientas, el identificador VirtualThread[#57] se pausa, liberando al worker-1 para que otra petición (como la del VirtualThread[#65]) pueda usar la CPU inmediatamente.

Simulando escenarios de alta concurrencia

Para ver el verdadero potencial, podemos exponer este servicio mediante un controlador REST muy sencillo. Si recibimos una gran cantidad de peticiones entrantes, podremos comprobar cómo el sistema responde de forma instantánea sin degradación del pool de conexiones:

package com.example.ai.mcp;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/agent")
public class AgentController {

    private final AgentService agentService;

    public AgentController(AgentService agentService) {
        this.agentService = agentService;
    }

    @PostMapping("/ask")
    public String ask(@RequestBody String prompt) {
        // Spring Boot mapea esta petición automáticamente en un hilo virtual
        return agentService.askAgent(prompt);
    }
}

Si hiciéramos una prueba de carga simulando 500 usuarios preguntando al agente en el mismo segundo (donde cada uno requiere que el LLM llame a herramientas que tardan 1.5 segundos en responder), un servidor tradicional con un pool de 200 hilos colapsaría por falta de recursos de ejecución.

Con este enfoque, la JVM creará 500 hilos virtuales ligeros que nacerán, esperarán el I/O del protocolo MCP sin penalización y morirán limpiamente tras entregar la respuesta. Todo en orden, todo correcto.

Algunos errores comunes y cómo podemos evitarlos

Los Virtual Threads no hacen que nuestros algoritmos de IA locales o el parseo de JSON sean más rápidos. El único poder (y no es poco) es optimizar los tiempos de espera de las entradas y salidas. Si nuestra tarea requiere procesar matrices pesadas en local, un hilo tradicional o un pool dedicado sigue siendo la opción adecuada.

Que nuestra aplicación pueda soportar miles de conexiones no significa que nuestra base de datos o el servidor MCP remoto que usamos pueda soportar 10.000 conexiones concurrentes. Debemos ajustar siempre los timeouts y los límites de los pools de conexiones de nuestros clientes HTTP/gRPC.

Al trabajar con herramientas de IA que disparan miles de hilos virtuales, es vital usar herramientas de monitoreo como JDK Flight Recorder (JFR) por ejemplo, para trackear que no se estén produciendo bloqueos indeseados en el planificador de la JVM.

Conclusión

Ya tenemos entre nuestras manos las piezas clave para construir la próxima generación de arquitecturas de IAl empresarial utilizando los estándares más actuales del sector. Unir la flexibilidad del MCP para desacoplar las herramientas de negocio, con la robustez y ligereza de los Virtual Threads en Java, nos permite derribar definitivamente el mito de que Java es un lenguaje pesado o lento para el desarrollo de ecosistemas ágiles de IA (¡larga vida a Java!).

Si venías del desarrollo web clásico, el cambio de mentalidad al diseñar agentes autónomos conectados por red es radical. Lo importante es experimentar, medir el comportamiento de la infraestructura bajo estrés y ajustar los flujos de herramientas. El camino hacia agentes de producción estables y masivos ya no es una utopía inalcanzable y con las próximas actualizaciones estoy seguro de que se irá avanzando aún más en este punto.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete