Artículo

Pruebas de Herramientas del Protocolo de Contexto de Modelo (MCP) en Spring AI

Pruebas de Herramientas del Protocolo de Contexto de Modelo (MCP) en Spring AI

1. Visión general

Modelo de Contexto de Modelo (MCP) es un protocolo estándar abierto que define cómo un modelo de lenguaje grande (LLM) descubre e invoca herramientas externas para ampliar sus capacidades. MCP es una arquitectura cliente-servidor que permite al cliente MCP, generalmente una aplicación integrada con LLM, interactuar con uno o más servidores MCP que exponen herramientas para su invocación.

Las herramientas expuestas por los servidores MCP son cruciales para validar que están correctamente registradas en los servidores MCP y son descubribles por los clientes MCP. A diferencia de las respuestas de los LLM, que son no deterministas, las herramientas MCP se comportan de manera determinista porque son simplemente código de aplicación normal, lo que nos permite escribir pruebas automatizadas para verificar la corrección.

En este tutorial, exploraremos cómo probar herramientas MCP en servidores MCP en Spring AI utilizando diferentes estrategias de prueba.

2. Dependencias de Maven

Probaremos las herramientas MCP en una aplicación Spring Boot. Por lo tanto, debemos agregar la dependencia del Servidor MCP de Spring AI a nuestro pom.xml:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
    <version>1.1.2</version>
</dependency>

También necesitaremos la dependencia de Spring Boot Test para nuestras pruebas:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. Creando una Herramienta MCP de Ejemplo

En esta sección, implementaremos una herramienta MCP simple utilizando Spring AI y demostraremos diferentes estrategias de prueba.

Primero, creemos un simple ExchangeRateService que utiliza un servicio de código abierto de terceros, Frankfurter, para obtener la tasa de cambio de moneda mediante una solicitud HTTP GET. Esta API requiere un parámetro de consulta obligatorio base:

@Service
public class ExchangeRateService {
    private static final String FRANKFURTER_URL = "https://api.frankfurter.dev/v1/latest?base={base}";

    private final RestClient restClient;

    public ExchangeRateService(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    public ExchangeRateResponse getLatestExchangeRate(String base) {
        if (base == null || base.isBlank()) {
            throw new IllegalArgumentException("base is required");
        }
        return restClient.get()
          .uri(FRANKFURTER_URL, base.trim().toUpperCase())
          .retrieve()
          .body(ExchangeRateResponse.class);
    }
}

La respuesta de la API se ve algo similar a:

{
  "amount": 1,
  "base": "GBP",
  "date": "2026-03-06",
  "rates": {
    "AUD": 1.9034,
    "BRL": 7.0366,
    ...
  }
}

Por lo tanto, creamos el siguiente registro Java para mapear la respuesta JSON:

public record ExchangeRateResponse(double amount, String base, String date, Map<String, Double> rates) {
}

Ahora, creemos una herramienta MCP que invoque el ExchangeRateService para devolver las tasas de cambio de moneda según la moneda base.

La descripción de la herramienta explica para qué sirve el parámetro, de modo que los clientes MCP sepan lo que deben proporcionar al llamarla:

@Component
public class ExchangeRateMcpTool {
    private final ExchangeRateService exchangeRateService;

    public ExchangeRateMcpTool(ExchangeRateService exchangeRateService) {
        this.exchangeRateService = exchangeRateService;
    }

    @McpTool(description = "Get latest exchange rates for a base currency")
    public ExchangeRateResponse getExchangeRate(
        @McpToolParam(description = "Base currency code, e.g. GBP, USD", required = true) String base) {
        return exchangeRateService.getLatestExchangeRate(base);
    }
}

4. Prueba Unitaria

Podríamos verificar la lógica de ExchangeRateMcpTool en aislamiento mediante una prueba unitaria. Por lo tanto, simulamos la dependencia externa para poder proporcionar una respuesta simulada.

El proceso de verificación es bastante sencillo para validar que el servicio se invoque correctamente y también que la respuesta se devuelva como se espera:

class ExchangeRateMcpToolUnitTest {
    @Test
    void whenBaseIsNotBlank_thenGetExchangeRateShouldReturnResponse() {
        ExchangeRateService exchangeRateService = mock(ExchangeRateService.class);
        ExchangeRateResponse expected = new ExchangeRateResponse(1.0, "GBP", "2026-03-08",
          Map.of("USD", 1.27, "EUR", 1.17));
        when(exchangeRateService.getLatestExchangeRate("gbp")).thenReturn(expected);

        ExchangeRateMcpTool tool = new ExchangeRateMcpTool(exchangeRateService);
        ExchangeRateResponse actual = tool.getExchangeRate("gbp");

        assertThat(actual).isEqualTo(expected);
        verify(exchangeRateService).getLatestExchangeRate("gbp");
    }
}

5. Creando un Cliente de Prueba MCP

Si queremos probar las herramientas MCP de extremo a extremo, podríamos crear un cliente MCP que se conecte al servidor MCP.

Los servidores MCP basados en HTTP exponen diferentes puntos finales según la propiedad de configuración del protocolo spring.ai.mcp.server.protocol en el application.yml. Spring AI utiliza SSE por defecto si no configuramos la propiedad explícitamente:

ProtocoloPunto final
Server‑Sent Events (SSE)/sse
Streamable HTTP/mcp

Además de los diferentes puntos finales, cada protocolo requiere una instancia diferente de McpClientTransport para crear el McpSyncClient.

Como Spring AI no proporciona una clase factory que cree automáticamente el cliente según el protocolo, creamos un componente de prueba, TestMcpClientFactory, que maneje la creación del McpSyncClient, para simplificar nuestras pruebas:

@Component
public class TestMcpClientFactory {
    private final String protocol;

    public TestMcpClientFactory(@Value("${spring.ai.mcp.server.protocol:sse}") String protocol) {
        this.protocol = protocol;
    }

    public McpSyncClient create(String baseUrl) {
        String resolvedProtocol = protocol.trim().toLowerCase();
        return switch (resolvedProtocol) {
            case "sse" -> McpClient.sync(HttpClientSseClientTransport.builder(baseUrl)
              .sseEndpoint("/sse")
              .build()
            ).build();
            case "streamable" -> McpClient.sync(HttpClientStreamableHttpTransport.builder(baseUrl)
              .endpoint("/mcp")
              .build()
            ).build();
            default -> throw new IllegalArgumentException("Unknown MCP protocol: " + protocol);
        };
    }
}

Soportamos únicamente los protocolos SSE y streamable en nuestra clase factory para demostrar la idea.

6. Verificando el Registro de la Herramienta

El servidor MCP expone un punto final HTTP para listar todas las herramientas disponibles que los clientes MCP pueden invocar. Por lo tanto, podríamos inicializar un cliente MCP para verificar el registro de la herramienta en el servidor MCP.
El siguiente es nuestro código base para inicializar y cerrar un McpSyncClient:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ExchangeRateMcpToolIntegrationTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestMcpClientFactory testMcpClientFactory;

    @MockBean
    private ExchangeRateService exchangeRateService;

    private McpSyncClient client;

    @BeforeEach
    void setUp() {
        client = testMcpClientFactory.create("http://localhost:" + port);
        client.initialize();
    }

    @AfterEach
    void cleanUp() {
        client.closeGracefully();
    }
}

Una vez que inicializamos un cliente MCP, invocamos el método listTools() del cliente para descubrir todas las herramientas que un servidor MCP ha registrado:

@Test
void whenMcpClientListTools_thenTheToolIsRegistered() {
    boolean registered = client.listTools().tools().stream()
      .anyMatch(tool -> Objects.equals(tool.name(), "getLatestExchangeRate"));
    assertThat(registered).isTrue();
}

La prueba devuelve una lista de herramientas registradas, y verificamos que getLatestExchangeRate sea una de ellas para confirmar la correcta inscripción.

7. Probando la Invocación de la Herramienta

Además, también podríamos verificar la herramienta MCP invocándola desde un cliente MCP. En esta prueba simulamos el ExchangeRateService para evitar realizar una llamada HTTP real al API de Frankfurter.

El flujo de invocación incluye descubrir las herramientas del servidor MCP, construir un CallToolRequest con todos los argumentos requeridos y llamarlo para obtener la respuesta del servidor:

@Test
void whenMcpClientCallTool_thenTheToolReturnsMockedResponse() {
    when(exchangeRateService.getLatestExchangeRate("GBP")).thenReturn(
      new ExchangeRateResponse(1.0, "GBP", "2026-03-08", Map.of("USD", 1.27))
    );

    McpSchema.Tool exchangeRateTool = client.listTools().tools().stream()
      .filter(tool -> "getLatestExchangeRate".equals(tool.name()))
      .findFirst()
      .orElseThrow();

    String argumentName = exchangeRateTool.inputSchema().properties().keySet().stream()
      .findFirst()
      .orElseThrow();

    McpSchema.CallToolResult result = client.callTool(
      new McpSchema.CallToolRequest("getLatestExchangeRate", Map.of(argumentName, "GBP"))
    );

    assertThat(result).isNotNull();
    assertThat(result.isError()).isFalse();
    assertTrue(result.toString().contains("GBP"));
}

Las aserciones aseguran que la llamada a la herramienta devuelve una respuesta válida sin errores.

8. Conclusiones

En este artículo, creamos una herramienta de servidor MCP de muestra, validamos su corrección, aseguramos que el servidor MCP la registrara y probamos la invocación de la herramienta mediante un cliente MCP.

Con pruebas unitarias e integradas en su lugar, podemos estar seguros de que la herramienta funciona correctamente y se expone adecuadamente para que los clientes MCP la invoquen.

Newsletter Semanal de Java

Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.

Sin spam. Cancela cuando quieras.

Compartir artículo