Ohhnews

分类导航

$ cd ..
Baeldung原文

在 Spring AI 中测试模型上下文协议 (MCP) 工具

#spring ai#mcp#自动化测试#后端开发#软件测试

1. 概述

模型上下文协议 (Model Context Protocol, MCP) 是一种开放标准协议,定义了大语言模型 (LLM) 如何发现并调用外部工具以扩展其能力。MCP 采用客户端-服务器架构,允许 MCP 客户端(通常是集成 LLM 的应用程序)与一个或多个公开工具供调用的 MCP 服务器进行交互。

测试 MCP 服务器公开的工具对于验证它们是否在 MCP 服务器上正确注册以及能否被 MCP 客户端发现至关重要。与非确定性的 LLM 响应不同,MCP 工具的行为是确定性的,因为它们本质上是普通的应用程序代码,这使我们能够编写自动化测试来验证其正确性。

在本教程中,我们将探讨如何在 Spring AI 中使用不同的测试策略来测试 MCP 服务器上的 MCP 工具。

2. Maven 依赖

我们将在 Spring Boot 应用程序中测试 MCP 工具。因此,必须在 pom.xml 中添加 Spring AI MCP 服务器 依赖:

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

我们还需要 Spring Boot Test 依赖来进行测试:

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

3. 创建示例 MCP 工具

在本节中,我们将使用 Spring AI 实现一个简单的 MCP 工具,并演示不同的测试策略。

首先,创建一个简单的 ExchangeRateService,它使用第三方开源服务 Frankfurter 通过 HTTP GET 请求获取货币汇率。该 API 需要一个强制性的查询参数 base

$ java
@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);
    }
}

API 响应示例如下:

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

因此,我们创建一个 Java record 来映射 JSON 响应:

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

现在,创建一个调用 ExchangeRateService 并根据基准货币返回汇率的 MCP 工具。

工具描述说明了参数的用途,以便 MCP 客户端在调用时知道应该提供什么:

$ java
@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. 单元测试

我们可以通过单元测试来独立验证 ExchangeRateMcpTool 的逻辑。因此,我们需要模拟(mock)外部依赖项,以便提供模拟的响应。

验证过程非常简单,旨在确认服务被正确调用且返回了预期的响应:

$ java
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. 创建 MCP 测试客户端

如果我们想进行 MCP 工具的端到端测试,可以创建一个连接到 MCP 服务器的 MCP 客户端。

基于 HTTP 的 MCP 服务器根据 application.yml 中的 spring.ai.mcp.server.protocol 配置属性公开不同的端点。如果我们没有显式设置该属性,Spring AI 默认使用 SSE:

协议端点
服务器发送事件 (SSE)/sse
可流式传输 HTTP/mcp

除了不同的端点外,每个协议都需要不同的 McpClientTransport 实例来创建 McpSyncClient

由于 Spring AI 没有提供基于协议自动创建客户端的工厂类,我们创建了一个测试组件 TestMcpClientFactory 来处理 McpSyncClient 的创建,以简化测试:

$ java
@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);
        };
    }
}

我们在工厂类中仅支持 SSE 和 streamable 协议以演示思路。

6. 验证工具注册

MCP 服务器公开了一个 HTTP 端点来列出所有可供 MCP 客户端调用的工具。因此,我们可以初始化一个 MCP 客户端来验证工具在 MCP 服务器上的注册情况。

以下是初始化和关闭 McpSyncClient 的基础代码:

$ java
@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();
    }
}

一旦初始化了 MCP 客户端,我们调用 listTools() 方法来查找 MCP 服务器注册的所有工具:

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

测试返回注册工具的列表,我们断言 getLatestExchangeRate 是其中之一,以确认注册成功。

7. 测试工具调用

此外,我们还可以通过从 MCP 客户端调用 MCP 工具来对其进行验证。在此测试中,我们模拟了 ExchangeRateService,以避免对 Frankfurter API 发起真实的 HTTP 调用。

调用流程包括:从 MCP 服务器发现工具、使用所有必需参数构建 CallToolRequest,并调用它以从服务器获取响应:

$ java
@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"));
}

这些断言确保工具调用返回了有效的响应且没有错误。

8. 总结

在本文中,我们创建了一个示例 MCP 服务器工具,验证了其正确性,确保了 MCP 服务器对其进行了注册,并通过 MCP 客户端测试了工具的调用。

通过单元测试和集成测试,我们可以确信该工具工作正常,并已正确公开,以便 MCP 客户端可以调用它。

完整的代码示例可在 GitHub 上获取。