Java SpringAI

Spring AI MCP Client — Connect Spring Boot to MCP Tool Servers

Spring AI MCP Client — Connect Spring Boot to MCP Tool Servers

An MCP client connects your Spring AI application to one or more MCP servers to use their tools. When your ChatClient is connected to an MCP server, the AI can automatically call any of the server's tools — whether they're on the same machine or running as remote services. This tutorial builds an MCP client that connects to multiple servers.

Maven Dependency

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

application.properties — Connect to MCP Servers

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini

# MCP Server 1: Weather tools server (running separately)
spring.ai.mcp.client.sse.connections.weather-server.url=http://localhost:8081

# MCP Server 2: Database tools server
spring.ai.mcp.client.sse.connections.db-server.url=http://localhost:8082

# MCP Server 3: Local stdio server (jar)
# spring.ai.mcp.client.stdio.connections.local-tools.command=java
# spring.ai.mcp.client.stdio.connections.local-tools.args=-jar,/path/to/tools.jar

McpClientService.java — Use MCP Tools in ChatClient

import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.spring.McpFunctionCallback;

@Service
public class McpClientService {

    private final ChatClient chatClient;

    // Spring AI auto-configures McpSyncClient beans for each server in properties
    public McpClientService(ChatClient.Builder builder,
                             List<McpSyncClient> mcpClients) {

        // Collect all tools from all connected MCP servers
        List<McpFunctionCallback> allTools = mcpClients.stream()
                .flatMap(client -> client.listTools()
                        .tools()
                        .stream()
                        .map(tool -> new McpFunctionCallback(client, tool)))
                .toList();

        this.chatClient = builder
                .defaultSystem("""
                    You are a helpful assistant with access to weather data,
                    database lookups, and Java documentation tools.
                    Use these tools to provide accurate, real-time information.
                    """)
                .defaultTools(allTools.toArray(new McpFunctionCallback[0]))
                .build();
    }

    public String ask(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

List Available Tools from MCP Server

@Service
public class McpToolDiscoveryService {

    private final List<McpSyncClient> mcpClients;

    public McpToolDiscoveryService(List<McpSyncClient> mcpClients) {
        this.mcpClients = mcpClients;
    }

    public Map<String, List<String>> listAllTools() {
        Map<String, List<String>> serverTools = new LinkedHashMap<>();

        for (McpSyncClient client : mcpClients) {
            List<String> toolNames = client.listTools().tools()
                    .stream()
                    .map(t -> t.name() + " — " + t.description())
                    .toList();
            serverTools.put(client.getServerInfo().name(), toolNames);
        }

        return serverTools;
    }

    public String readResource(McpSyncClient client, String resourceUri) {
        var result = client.readResource(
                new McpSchema.ReadResourceRequest(resourceUri));
        return result.contents().stream()
                .filter(c -> c instanceof McpSchema.TextResourceContents)
                .map(c -> ((McpSchema.TextResourceContents) c).text())
                .collect(Collectors.joining("\n"));
    }
}

Controller

@RestController
@RequestMapping("/mcp")
public class McpClientController {

    private final McpClientService        clientService;
    private final McpToolDiscoveryService discovery;

    public McpClientController(McpClientService clientService,
                                McpToolDiscoveryService discovery) {
        this.clientService = clientService;
        this.discovery     = discovery;
    }

    @GetMapping("/ask")
    public String ask(@RequestParam String q) {
        return clientService.ask(q);
    }

    @GetMapping("/tools")
    public Map<String, List<String>> listTools() {
        return discovery.listAllTools();
    }
}

Interaction Demo

# GET /mcp/tools
{
  "weather-server": [
    "getCurrentWeather — Get the current weather conditions for a given city",
    "getWeatherForecast — Get the weather forecast for a city for the next N days"
  ],
  "db-server": [
    "findUserByEmail — Find a user by their email address",
    "getUserOrderSummary — Get total order count and revenue for a user ID"
  ]
}

# GET /mcp/ask?q=What's the weather in Tokyo and find user ravi@example.com
Response:
  The current weather in Tokyo is 28°C, humid and overcast.

  For the user ravi@example.com:
  - ID: 42, Name: Ravi Kumar, Registered: 2023-01-15
  - They have placed 8 orders totaling $487.50

Using MCP Prompts via Client

@Service
public class McpPromptService {

    private final McpSyncClient mcpClient;

    public McpPromptService(@Qualifier("java-tools-server") McpSyncClient mcpClient) {
        this.mcpClient = mcpClient;
    }

    public String runJavaCodeReview(String code, String focusArea) {
        // Get the pre-defined prompt from the MCP server
        var promptResult = mcpClient.getPrompt(
                new McpSchema.GetPromptRequest(
                        "java-code-review",
                        Map.of("code", code, "focus_area", focusArea)
                )
        );

        // Extract the prompt messages
        String promptContent = promptResult.messages().stream()
                .filter(m -> m.role() == McpSchema.Role.USER)
                .map(m -> ((McpSchema.TextContent) m.content()).text())
                .collect(Collectors.joining("\n"));

        return promptContent; // Use this with ChatClient
    }
}

Key Points

  • Spring AI auto-creates one McpSyncClient bean per server entry in application.properties — inject as List<McpSyncClient> to get all of them
  • McpFunctionCallback wraps an MCP tool as a Spring AI ToolCallback — register them with defaultTools() on ChatClient
  • For async/reactive applications use McpAsyncClient instead of McpSyncClient
  • MCP servers can be on separate machines — the client communicates over HTTP/SSE; use stdio for local subprocess servers
  • Tools from MCP servers are dynamically discovered at runtime — no code changes needed when the server adds new tools
Topics: Java SpringAI
← Newer Post Older Post →