Enterprise Java

Spring AI: Testing MCP Tools

Testing Model Context Protocol (MCP) tools in Spring AI is essential to ensure that tools are correctly registered, discoverable, and invocable by MCP clients. Unlike LLM-generated responses, MCP tools are deterministic because they encapsulate standard application logic. This determinism allows for precise and repeatable automated tests. This article provides a guide to testing MCP tools in Spring AI.

1. Project Dependencies

The following dependencies enable MCP server support and testing capabilities within a Spring Boot application.

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

The configuration above includes the Spring AI MCP server starter, which enables tool exposure via MCP, and the Spring Boot testing starter, which provides JUnit, Mockito, and assertion libraries.

2. Building a Weather Service Integration

Before exposing any functionality through an MCP tool, first, we build a reliable service layer that encapsulates external API communication. In this case, the service is responsible for fetching live weather data from the Open-Meteo API using latitude and longitude coordinates.

The following implementation uses Spring’s RestClient to call the Open-Meteo forecast endpoint.

@Service
public class WeatherService {

    private static final String WEATHER_URL ="https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m";

    private final RestClient restClient;

    public WeatherService(RestClient.Builder builder) {
        this.restClient = builder.build();
    }

    public WeatherResponse getCurrentWeather(double latitude, double longitude) {
        return restClient.get()
                .uri(WEATHER_URL, latitude, longitude)
                .retrieve()
                .body(WeatherResponse.class);
    }
}

This service defines a constant URL template where latitude and longitude are injected dynamically at runtime. The RestClient is created via constructor injection using a builder, which keeps it flexible and testable.

The getCurrentWeather method is the core of this service. It performs an HTTP GET request, substitutes the path variables into the URL, and maps the JSON response directly into a WeatherResponse record. To deserialize the JSON response from the weather API, we define the WeatherResponse Java record.

public record WeatherResponse(double latitude, double longitude, Current current) {

    public record Current(double temperature) {

    }
}

3. Creating the MCP Tool

The MCP tool acts as the bridge between the Model Context Protocol runtime and our business logic. It defines how external clients can invoke operations, what parameters are required, and how those parameters are interpreted at runtime. In this case, the goal is to expose a simple weather retrieval capability that accepts latitude and longitude and returns structured weather data.

The implementation below wraps the WeatherService and annotates the method with MCP-specific metadata so that it can be discovered and invoked by MCP clients.

@Component
public class WeatherMcpTool {

    private final WeatherService weatherService;

    public WeatherMcpTool(WeatherService weatherService) {
        this.weatherService = weatherService;
    }

    @McpTool(description = "Retrieve current weather using latitude and longitude")
    public WeatherResponse getWeather(
            @McpToolParam(description = "Latitude value", required = true) double latitude,
            @McpToolParam(description = "Longitude value", required = true) double longitude) {

        return weatherService.getCurrentWeather(latitude, longitude);
    }
}

The @McpTool annotation defines the tool’s name and description, which is what MCP clients will see when listing available tools. Each parameter is annotated with @McpToolParam, allowing MCP to generate a proper input schema with validation rules such as required fields and descriptions.

4. Unit Testing the MCP Tool

Unit testing the MCP tool focuses on verifying that the tool behaves correctly as a thin orchestration layer between the MCP runtime and the underlying service. The goal of the test is not to validate external API calls, but to ensure that inputs are correctly passed through and responses are properly returned from the service layer.

The following unit test uses Mockito to mock the WeatherService dependency. It verifies that when valid latitude and longitude values are provided, the MCP tool delegates the call correctly and returns the expected response without modifying it.

class WeatherMcpToolUnitTest {

    @Test
    void whenValidCoordinates_thenReturnsWeatherResponse() {
        WeatherService service = mock(WeatherService.class);

        WeatherResponse expected
                = new WeatherResponse(new WeatherResponse.CurrentWeather(25.5, 10.2));

        when(service.getCurrentWeather(10.0, 20.0)).thenReturn(expected);

        WeatherMcpTool tool = new WeatherMcpTool(service);

        WeatherResponse actual = tool.getWeather(10.0, 20.0);

        assertThat(actual).isEqualTo(expected);
        verify(service).getCurrentWeather(10.0, 20.0);
    }
}

The assertion confirms that the returned value matches what the service provides, while the verification step ensures that the service method is invoked exactly once with the expected arguments.

5. End-to-End Integration Testing of the MCP Tool

Integration testing in an MCP-based system validates the complete runtime flow: from MCP client initialization, through tool discovery, to actual tool invocation and response validation. Unlike unit tests, which isolate components, integration tests ensure that Spring Boot, MCP transport, annotation scanning, and tool execution all work together correctly in a real server environment.

To support a reusable test setup, a dedicated McpTestClientProvider class is created. It abstracts away the complexity of choosing between SSE and Streamable HTTP transport and ensures that the correct MCP client is consistently created across all integration tests.

@Component
public class McpTestClientProvider {

    private final String protocol;

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

    private final HttpClient.Builder clientBuilder = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .followRedirects(HttpClient.Redirect.NORMAL);

    public McpSyncClient create(String baseUrl) {

        String resolvedProtocol = protocol.trim().toLowerCase();

        return switch (resolvedProtocol) {

            case "sse" -> McpClient.sync(
                    HttpClientSseClientTransport.builder(baseUrl)
                            .clientBuilder(clientBuilder)
                            .sseEndpoint("/sse")   
                            .build()
            ).build();

            case "streamable" -> McpClient.sync(
                    HttpClientStreamableHttpTransport.builder(baseUrl)
                            .clientBuilder(clientBuilder)
                            .endpoint("/mcp")      
                            .build()
            ).build();

            default -> throw new IllegalArgumentException("Unrecognized MCP protocol: " + protocol);
        };
    }
}

Weather MCP Tool Integration Test

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WeatherMcpToolIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private McpTestClientProvider clientProvider;

    @MockitoBean
    private WeatherService weatherService;

    private McpSyncClient client;

    @BeforeEach
    void setUp() {
        IO.println("MCP Test Server running on port: " + port);
        client = clientProvider.create("http://localhost:" + port);

        client.initialize();
    }

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

    @Test
    void whenMcpClientListTools_thenToolIsRegistered() {
        boolean registered = client.listTools()
                .tools()
                .stream()
                .anyMatch(tool -> "getWeather".equals(tool.name()));

        assertThat(registered).isTrue();
    }

    @Test
    void whenMcpClientCallTool_thenReturnsMockedResponse() {
        WeatherResponse expected = new WeatherResponse(10.0, 20.0, new WeatherResponse.Current(30.0));

        when(weatherService.getCurrentWeather(10.0, 20.0))
                .thenReturn(expected);

        McpSchema.CallToolResult result = client.callTool(
                new McpSchema.CallToolRequest("getWeather", Map.of("latitude", 10.0, "longitude", 20.0))
        );

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

This integration test verifies the full MCP lifecycle in a running Spring Boot context. The test starts an embedded server on a random port, initializes an MCP client using the provider, and confirms that the getWeather tool is properly registered and invokable.

The first test ensures that tool discovery works correctly by validating that the MCP runtime exposes the getWeather tool. The second test mocks the WeatherService, invoking the tool through the MCP client, and validating that the response flows correctly through the MCP transport layer. This confirms that tool registration, request mapping, and response serialization all function correctly in a real MCP execution environment.

MCP Server Configuration

spring.main.banner-mode=off

spring.ai.mcp.server.name=weather-mcp-server
spring.ai.mcp.server.annotation-scanner.enabled=true
spring.ai.mcp.server.transport=WEBMVC
spring.ai.mcp.server.title=Weather MCP SSE Server
spring.ai.mcp.server.version=0.0.1

spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.protocol=SSE
spring.ai.mcp.server.sse-endpoint=/sse
spring.ai.mcp.server.sse-message-endpoint=/mcp/message

spring.ai.mcp.server.capabilities.tool=true
spring.ai.mcp.server.capabilities.resource=true
spring.ai.mcp.server.capabilities.prompt=true
spring.ai.mcp.server.capabilities.completion=true
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.expose-mcp-client-tools=true

This configuration enables the MCP server capabilities required for integration testing, including tool discovery, SSE transport, and annotation scanning. It ensures that the MCP runtime is fully active during tests, allowing tools to be registered and invoked exactly as they would be in production.

6. Conclusion

In this article, we explored how to build and validate an MCP-enabled weather tool using Spring, demonstrating how to connect an external API to an MCP tool and expose it through a functional server. We started by designing a service layer for interacting with the Open-Meteo API, then wrapped that functionality in an MCP tool that defines a contract for clients. From there, we implemented both unit and integration tests to ensure correctness at every level, from simple method delegation to full end-to-end MCP communication.

7. Download the Source Code

This article explored testing MCP tools in Spring AI.

Download
You can download the full source code of this example here: spring ai testing mcp tools

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button