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
McpSyncClientbean per server entry inapplication.properties— inject asList<McpSyncClient>to get all of them McpFunctionCallbackwraps an MCP tool as a Spring AIToolCallback— register them withdefaultTools()on ChatClient- For async/reactive applications use
McpAsyncClientinstead ofMcpSyncClient - 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
Comments