Spring AI 中的模型情境協定 (MCP) 測試工具
1. 概述
模型上下文協議 (MCP) 是一種開放標準協議,它定義了大型語言模型 (LLM) 如何發現並調用外部工具來擴展其功能。 MCP採用客戶端-伺服器架構,允許 MCP 客戶端(通常是與 LLM 整合的應用程式)與一個或多個 MCP 伺服器交互,這些伺服器公開可供呼叫的工具。
MCP 伺服器提供的測試工具對於驗證它們是否已正確註冊到 MCP 伺服器並可被 MCP 用戶端發現至關重要。與不確定的 LLM 回應不同,MCP 工具的行為是確定性的,因為它們只是普通的應用程式程式碼,這使得我們可以編寫自動化測試來驗證其正確性。
在本教學中,我們將探討如何使用不同的測試策略在 Spring AI 的 MCP 伺服器上測試 MCP 工具。
2. Maven 依賴項
我們將在 Spring Boot 應用程式中測試 MCP 工具。因此,我們必須將Spring AI MCP 伺服器依賴項新增至pom.xml檔案中:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.1.2</version>
</dependency>
我們還需要Spring Boot Test依賴項來進行測試:
<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 :
@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 回應類似於:
{
"amount": 1,
"base": "GBP",
"date": "2026-03-06",
"rates": {
"AUD": 1.9034,
"BRL": 7.0366,
......
}
}
因此,我們建立以下 Java 記錄來對應 JSON 回應:
public record ExchangeRateResponse(double amount, String base, String date, Map<String, Double> rates) {
}
現在,讓我們建立一個 MCP 工具,該工具呼叫ExchangeRateService以根據基礎貨幣返回貨幣匯率。
工具說明解釋了參數的用途,以便 MCP 用戶端知道在呼叫該工具時應該提供哪些參數:
@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, eg GBP, USD", required = true) String base) {
return exchangeRateService.getLatestExchangeRate(base);
}
}
4. 單元測試
我們可以透過單元測試來單獨驗證ExchangeRateMcpTool邏輯。因此,我們模擬外部依賴項,以便提供模擬響應。
驗證過程相當簡單,只需驗證服務是否正確調用,以及回應是否如預期返回:
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 |
| Streamable HTTP | /mcp |
除了不同的端點之外,每個協定都需要不同的McpClientTransport實例來建立McpSyncClient 。
由於 Spring AI 沒有提供根據協議自動創建客戶端的工廠類,我們創建了一個測試組件TestMcpClientFactory,來處理McpSyncClient創建,以簡化我們的測試:
@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 和可流協議,以演示該概念。
6. 驗證工具註冊
MCP 伺服器公開了一個 HTTP 端點,用於列出 MCP 用戶端可以呼叫的所有可用工具。因此,我們可以初始化一個 MCP 用戶端來驗證該工具是否已在 MCP 伺服器上註冊。
以下是我們用於初始化和關閉的基本程式碼 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();
}
}
一旦我們初始化了 MCP 用戶端,我們就呼叫客戶端的listTools()方法來尋找 MCP 伺服器已註冊的所有工具:
@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 ,並呼叫它以取得伺服器的回應:
@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 上找到。