SPRING-AI

Integration Testing Spring AI MCP Server

While working on my ZDF Mediathek MCP Server, I noticed a lack of good resources on how to properly integration test a Spring AI MCP Server. Since I usually follow a Test-Driven Development (TDD) approach, this was a bit of a hurdle.

I wanted to test my MCP tools integratively, meaning I wanted to ensure that the part I’m testing is actually called via the Model Context Protocol.

The Approach

My solution involves using the Spring AI MCP Client within the test to establish a connection to the MCP Server that is started via @SpringBootTest. This allows me to simulate a real client interaction.

To make this work, I first needed to add the MCP Client dependency to my build.gradle.kts:

testImplementation("org.springframework.ai:spring-ai-starter-mcp-client-webflux")

The Integration Test

Here is an example based on my SearchContentServiceIT, which tests the search_content tool.

The key part is the setUp and tearDown methods. In setUp, I initialize the mcpClient so that it connects to the server running on the random local port provided by @SpringBootTest.

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = [
        "spring.ai.mcp.client.enabled=true",
        "spring.ai.mcp.client.type=async",
        // ... other properties
    ]
)
@EnableWireMock(
    ConfigureWireMock(
        baseUrlProperties = ["zdf.url"]
    )
)
class SearchContentServiceIT {
    @LocalServerPort
    private var port: Int = 0

    @Autowired
    private lateinit var webClientBuilder: WebClient.Builder

    private lateinit var mcpClient: McpAsyncClient

    @BeforeEach
    fun setUp() {
        val transport = WebClientStreamableHttpTransport.builder(
            webClientBuilder.baseUrl("http://localhost:$port")
        )
            .endpoint("/")
            .build()

        mcpClient = McpClient.async(transport).build()
        mcpClient.initialize().block()
    }

    @AfterEach
    fun tearDown() {
        mcpClient.closeGracefully().block()
    }

    // ... tests
}

I use WireMock to mock the actual external API (in this case, the ZDF API) that my MCP tool talks to. This keeps the test isolated from the real backend while testing the full MCP flow.

Calling the Tool

In the test method itself, I can now use the mcpClient to call the tool and verify the result.

    @Test
    fun `search_content valid Query returns expected result`() {
        // ... setup expectations and WireMock stubs ...

        // when
        val response = parseTextContent(
            mcpClient.callTool(
                McpSchema.CallToolRequest(
                    "search_content",
                    mapOf<String, String>(
                        Pair("query", "Tagesschau"),
                        Pair("limit", "2")
                    )
                )
            )
        )

        // then
        assertThat(response).usingRecursiveComparison().isEqualTo(expectedResponse)
    }

Parsing the Response

One interesting helper method I created is parseTextContent. It helps to convert the text content from the MCP Client’s response back into a typed object using the Jackson ObjectMapper. This makes assertions much easier.

    private fun parseTextContent(result: Mono<McpSchema.CallToolResult>): ZdfSearchResponse {
        return objectMapper.readValue<ZdfSearchResponse>(
            (result.block()!!
                .content()
                .first() as McpSchema.TextContent).text()
        )
    }

This approach allows me to test my MCP tools end-to-end within the Spring context, ensuring that the MCP layer, the tool implementation, and the integration with external services (mocked) work together as expected.