Why Build an MCP Server with Spring Boot?
MCP (Model Context Protocol) is a standard protocol for AI to call external tools. While FastMCP is widely used in the Python ecosystem, if you’re already running Spring Boot-based services, using Spring AI’s MCP starter is the natural choice.
Simply adding annotations to existing Spring Bean methods turns them into an MCP server. You can expose existing business logic to AI without any additional infrastructure.
Dependency Setup
Add the Spring AI BOM and MCP Server starter.
Maven
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- STDIO transport (for Claude Desktop connection) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<!-- HTTP/SSE transport (for remote server) -->
<!--
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
-->
</dependencies>
Gradle
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:1.1.4"
}
}
dependencies {
implementation "org.springframework.ai:spring-ai-starter-mcp-server"
}
application.properties
When using STDIO transport, console output must be disabled. If Spring banners or logs print to stdout, they will corrupt JSON-RPC messages.
spring.application.name=my-mcp-server
# MCP server configuration
spring.ai.mcp.server.name=my-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=SYNC
# Required for STDIO transport -- block console output
spring.main.web-application-type=none
spring.main.banner-mode=off
logging.pattern.console=
logging.file.name=./logs/mcp-server.log
When using HTTP/SSE transport, remove the web-application-type and banner-mode settings, and use the spring-ai-starter-mcp-server-webmvc dependency instead.
Implementing Tools
Expose Spring Bean methods as MCP Tools using the @Tool annotation.
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class TodoService {
public record TodoItem(String id, String title, boolean done, String createdAt) {}
private final Map<String, TodoItem> todos = new ConcurrentHashMap<>();
private int nextId = 1;
@Tool(name = "createTodo", description = "Creates a new todo item")
public String createTodo(
@ToolParam(description = "Todo title") String title) {
String id = String.valueOf(nextId++);
String now = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
todos.put(id, new TodoItem(id, title, false, now));
return "Todo #" + id + " '" + title + "' created";
}
@Tool(name = "listTodos", description = "Lists all todo items")
public List<TodoItem> listTodos() {
return new ArrayList<>(todos.values());
}
@Tool(name = "completeTodo", description = "Marks a todo item as complete")
public String completeTodo(
@ToolParam(description = "ID of the todo to complete") String id) {
TodoItem item = todos.get(id);
if (item == null) {
return "Todo #" + id + " not found";
}
todos.put(id, new TodoItem(item.id(), item.title(), true, item.createdAt()));
return "Todo #" + id + " '" + item.title() + "' marked as complete";
}
@Tool(name = "deleteTodo", description = "Deletes a todo item")
public String deleteTodo(
@ToolParam(description = "ID of the todo to delete") String id) {
TodoItem item = todos.remove(id);
if (item == null) {
return "Todo #" + id + " not found";
}
return "Todo #" + id + " '" + item.title() + "' deleted";
}
}
The description in @ToolParam is passed to the AI, so clearly describe what each parameter means.
Registering Tools with MCP
In the Spring Boot main class, use ToolCallbacks.from() to auto-register the Bean’s Tool methods.
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.List;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public List<ToolCallback> todoTools(TodoService todoService) {
return ToolCallbacks.from(todoService);
}
}
ToolCallbacks.from(bean) scans all methods annotated with @Tool in the Bean and registers them as MCP Tools. If you have multiple Service Beans, register each one separately.
@Bean
public List<ToolCallback> allTools(
TodoService todoService,
WeatherService weatherService) {
var tools = new ArrayList<ToolCallback>();
tools.addAll(ToolCallbacks.from(todoService));
tools.addAll(ToolCallbacks.from(weatherService));
return tools;
}
Build and Run
# Build JAR with Maven
./mvnw clean package -DskipTests
# Run (STDIO mode will wait for input rather than exiting immediately)
java -jar target/mcp-server-0.0.1-SNAPSHOT.jar
# Gradle
./gradlew bootJar
java -jar build/libs/mcp-server-0.0.1-SNAPSHOT.jar
Connecting to Claude Desktop
Register the built JAR in the Claude Desktop configuration.
Config file location:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"todo-server": {
"command": "java",
"args": [
"-jar",
"/path/to/mcp-server-0.0.1-SNAPSHOT.jar"
]
}
}
}
Connecting from Claude Code:
claude mcp add todo-server -- java -jar /path/to/mcp-server-0.0.1-SNAPSHOT.jar
claude mcp list
After configuration, restart Claude Desktop and the AI will recognize the createTodo, listTodos, completeTodo, and deleteTodo Tools.
Switching to HTTP/SSE Transport
To run as a remote server, switch to the WebMVC starter.
<!-- pom.xml: Use WebMVC starter instead of STDIO starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
# application.properties
spring.ai.mcp.server.name=my-mcp-server
spring.ai.mcp.server.version=1.0.0
# Remove web-application-type=none (enable web server)
# Remove banner-mode=off
# Remove logging.pattern.console=
server.port=8080
Connecting to a remote server from Claude Desktop:
{
"mcpServers": {
"todo-server-remote": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8080/sse"]
}
}
}
STDIO vs HTTP Comparison
| Item | STDIO | HTTP/SSE |
|---|---|---|
| Transport | Standard I/O streams | HTTP POST + Server-Sent Events |
| Network | Not required (local process) | Required (can be remote) |
| Concurrent connections | Single client | Multiple clients |
| Authentication | None (local) | OAuth, API Key, etc. |
| Deployment | Run JAR file directly | Deploy as regular Spring Boot server |
| Use case | Local development, Claude Desktop | Production, team-shared server |
Summary
| Step | Details |
|---|---|
| 1. Dependencies | spring-ai-starter-mcp-server (STDIO) or -webmvc (HTTP) |
| 2. Configuration | Set server name and transport type in application.properties |
| 3. Implement Tools | @Tool annotation on @Service Bean methods |
| 4. Register | Return ToolCallbacks.from(service) as a @Bean |
| 5. Build | mvnw package to generate JAR |
| 6. Connect | Register JAR path in Claude Desktop config |
Adding MCP to an existing Spring Boot service only requires two things: @Tool annotations and ToolCallbacks.from() Bean registration. Without modifying existing business logic, simply adding annotations above Service methods turns them into tools that AI can call.