Build a Production-Ready MCP Server with Spring Boot 4 & Spring AI 1.1
Give any AI agent full CRUD control over a database — in pure Java, zero Python.
Why This Matters
The Model Context Protocol (MCP) landed in late 2024 and spread fast. Within months, every major AI client — Claude Desktop, Cursor, Windsurf, and dozens of others — adopted it as the standard way for LLMs to talk to external tools.
The problem? Nearly all the tutorials are Python.
The Java ecosystem is massively underserved here. If you're running Spring Boot microservices — and a huge portion of the enterprise world is — you deserve a first-class, idiomatic MCP story. That's what this article delivers.
By the end, you'll have:
A fully working MCP server exposing 9 Todo management tools
A Spring AI 1.1.2 + Spring Boot 4.0.x setup using the official starters
Clean service/tools separation so your business logic stays testable
Unit tests with JUnit 5 + Mockito
A clear mental model of how MCP fits into your architecture
What Is MCP? (The 90-Second Version)
MCP is a client–server protocol that lets AI models call external tools in a standardised way. Think of it as USB-C for AI integrations — one protocol, any tool, any client.
MCP Client (Claude Desktop / Cursor / your app)
│
│ SSE or STDIO transport
│
MCP Server (your Spring Boot app)
│
├── Tool: createTodo
├── Tool: getAllTodos
├── Tool: completeTodo
└── Tool: getStats
When a user asks Claude "What are my critical tasks today?", the LLM:
Recognizes it needs external data
Calls your
getTodosByPrioritytool withpriority="CRITICAL"Reads the JSON response
Formulates a natural language answer
Your Java code runs. The AI gets the data. The user gets a useful answer. No hallucination, no guessing.
Project Overview
Stack:
Spring Boot 4.0.3
Spring AI 1.1.2 (latest stable as of March 2026)
Spring Data JPA + H2 (swap to PostgreSQL for production)
Java 25
MCP Tools exposed:
| Tool | Description |
|---|---|
createTodo |
Create a new task with title, description, priority |
getTodoById |
Fetch a single task by id |
getAllTodos |
List all tasks |
getTodosByStatus |
Filter by PENDING / IN_PROGRESS / COMPLETED / CANCELLED |
getTodosByPriority |
Filter by LOW / MEDIUM / HIGH / CRITICAL |
searchTodos |
Full-text search on title and description |
updateTodo |
Partial update — only pass fields you want to change |
completeTodo |
Mark a task done, records completion timestamp |
deleteTodo |
Permanently remove a task |
getStats |
Aggregate counts — great for dashboard summaries |
Project Structure
todo-mcp-server/
├── pom.xml
└── src/main/java/com/tanmoymandal/mcp/todo/
├── TodoMcpServerApplication.java ← main class
├── model/
│ └── Todo.java ← JPA entity + enums
├── repository/
│ └── TodoRepository.java ← Spring Data JPA
├── service/
│ └── TodoService.java ← business logic (no MCP dependency)
├── tools/
│ └── TodoMcpTools.java ← @Tool annotations live here
└── config/
├── McpServerConfig.java ← registers tools with MCP
└── DataInitializer.java ← seeds demo data on startup
Key design decision:
TodoServicehas zero dependency on Spring AI. TheTodoMcpToolsclass is the adapter between the AI world and your domain. This separation keeps your business logic independently testable and reusable outside of MCP context.
Step 1 — The pom.xml
The single most important thing here is the Spring AI BOM — it ensures all Spring AI artifacts are version-compatible with no manual version juggling.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.3</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- MCP Server over HTTP/SSE using Spring MVC -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<!-- Web layer (required by webmvc transport) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA + H2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Why
spring-ai-starter-mcp-server-webmvc? This starter provides the HTTP/SSE transport layer — the way most MCP clients connect to remote servers. It auto-configures the/sseand/mcp/messagesendpoints for you. The alternativespring-ai-starter-mcp-serveris STDIO only (suitable for local subprocess invocation). Usewebmvcfor anything network-accessible.
Step 2 — application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:tododb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
h2:
console:
enabled: true
ai:
mcp:
server:
name: todo-mcp-server
version: 1.0.0
type: SYNC
instructions: |
This server manages Todo/Task items.
Tools available: createTodo, getTodoById, getAllTodos,
updateTodo, deleteTodo, completeTodo, searchTodos,
getTodosByPriority, getStats.
capabilities:
tool: true
The instructions field is important — it's sent to the AI client during the handshake and helps the LLM understand what your server does before it even reads the individual tool descriptions.
Step 3 — The Entity
@Entity
@Table(name = "todos")
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String title;
@Column(length = 2000)
private String description;
@Enumerated(EnumType.STRING)
private Priority priority = Priority.MEDIUM;
@Enumerated(EnumType.STRING)
private Status status = Status.PENDING;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime completedAt;
public enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
public enum Status { PENDING, IN_PROGRESS, COMPLETED, CANCELLED }
@PrePersist
protected void onCreate() {
createdAt = updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// ... getters/setters
}
Step 4 — The Tools Class (The Heart of It All)
This is where Spring AI's @Tool annotation does the heavy lifting. Every annotated method becomes a discoverable MCP tool with an auto-generated JSON Schema for its parameters.
@Service
public class TodoMcpTools {
private final TodoService service;
private final ObjectMapper objectMapper;
public TodoMcpTools(TodoService service, ObjectMapper objectMapper) {
this.service = service;
this.objectMapper = objectMapper;
}
@Tool(name = "createTodo",
description = """
Creates a new Todo item. Returns the created Todo as JSON
including the auto-generated id.
Priority: LOW, MEDIUM, HIGH, CRITICAL (defaults to MEDIUM).
""")
public String createTodo(
@ToolParam(description = "Short, descriptive title. Required.")
String title,
@ToolParam(description = "Optional detailed description.", required = false)
String description,
@ToolParam(description = "Priority: LOW, MEDIUM, HIGH, CRITICAL.", required = false)
String priority) {
try {
Todo todo = service.create(title, description, priority);
return toJson(todoToMap(todo));
} catch (Exception e) {
return errorJson("createTodo failed: " + e.getMessage());
}
}
@Tool(name = "completeTodo",
description = """
Marks a Todo as COMPLETED and records the completion timestamp.
Returns the updated Todo as JSON.
""")
public String completeTodo(
@ToolParam(description = "The numeric id of the todo to complete.")
Long id) {
return service.complete(id)
.map(t -> toJson(todoToMap(t)))
.orElse(errorJson("Todo with id=%d not found".formatted(id)));
}
@Tool(name = "getStats",
description = """
Returns aggregate statistics: counts by status and priority.
Great for summaries and dashboards.
""")
public String getStats() {
return toJson(service.getStats());
}
// ... remaining tools follow the same pattern
}
Write tool descriptions for the LLM, not for humans. Be explicit about valid enum values, what null means, and exactly what the return value contains. The LLM reads these descriptions to decide which tool to call and how to call it correctly.
Step 5 — Register the Tools
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider todoToolCallbacks(TodoMcpTools todoMcpTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(todoMcpTools)
.build();
}
}
That's it. MethodToolCallbackProvider reflects over your @Tool methods, generates the MCP tool descriptors with full JSON Schema from the method signatures, and Spring AI's auto-configuration registers them on the MCP endpoint automatically.
Step 6 — Run It
./mvnw spring-boot:run
You'll see in the logs:
Registered MCP tools: [createTodo, getTodoById, getAllTodos,
getTodosByStatus, getTodosByPriority, searchTodos,
updateTodo, completeTodo, deleteTodo, getStats]
Started TodoMcpServerApplication on port 8080
The server is live at:
SSE endpoint:
http://localhost:8080/sseH2 Console:
http://localhost:8080/h2-consoleHealth check:
http://localhost:8080/actuator/health
Connecting Claude Desktop
Add this to your Claude Desktop claude_desktop_config.json:
{
"mcpServers": {
"todo-manager": {
"type": "sse",
"url": "http://localhost:8080/sse"
}
}
}
Restart Claude Desktop. You'll see a hammer icon in the chat UI — that means your tools are live. Now ask:
"What are my critical priority tasks?""Mark task 5 as complete.""Give me a summary of all my todos."
Claude will call your Spring Boot server, get real data back, and answer with zero hallucination.
Unit Testing the Service Layer
Because TodoService has no Spring AI dependency, it tests exactly like any other Spring service:
@ExtendWith(MockitoExtension.class)
class TodoServiceTest {
@Mock TodoRepository repository;
@InjectMocks TodoService service;
@Test
void create_shouldPersistAndReturnTodo() {
Todo expected = new Todo("Fix bug", "NPE on line 42", Todo.Priority.HIGH);
expected.setId(1L);
when(repository.save(any())).thenReturn(expected);
Todo result = service.create("Fix bug", "NPE on line 42", "HIGH");
assertThat(result.getTitle()).isEqualTo("Fix bug");
assertThat(result.getPriority()).isEqualTo(Todo.Priority.HIGH);
verify(repository).save(any(Todo.class));
}
@Test
void complete_shouldSetStatusAndTimestamp() {
Todo todo = new Todo("Task", null, Todo.Priority.MEDIUM);
todo.setId(1L);
when(repository.findById(1L)).thenReturn(Optional.of(todo));
when(repository.save(any())).thenAnswer(i -> i.getArguments()[0]);
Optional<Todo> result = service.complete(1L);
assertThat(result).isPresent();
assertThat(result.get().getStatus()).isEqualTo(Todo.Status.COMPLETED);
assertThat(result.get().getCompletedAt()).isNotNull();
}
}
Production Considerations
When you're ready to move beyond the demo, here's what to address:
Database: Swap H2 for PostgreSQL by replacing the datasource config and adding the PostgreSQL driver. Zero code changes needed — that's the beauty of Spring Data JPA.
Security: The MCP spec requires OAuth2 for HTTP-exposed servers. Spring AI 1.1.x has a companion mcp-server-security module. Add it and a SecurityFilterChain bean — the Spring team published a detailed walkthrough on the Spring blog.
Observability: Add spring-boot-starter-actuator with Micrometer. Your MCP tool call counts, latencies, and error rates surface as Prometheus metrics automatically.
Packaging: Build a Docker image with ./mvnw spring-boot:build-image — Spring Boot's Cloud Native Buildpacks produce a production-grade container with no Dockerfile required.
What We Built
In one Spring Boot application we've produced an MCP server that:
Exposes 9 fully-described tools discoverable by any MCP client
Uses standard Spring idioms — JPA,
@Service,@Bean,@TransactionalKeeps business logic completely decoupled from the AI/MCP layer
Is unit-testable without any AI dependencies
Seeds itself with demo data for instant exploration
Connects to Claude Desktop, Cursor, or any MCP-compatible client in 30 seconds
The Java ecosystem is ready for MCP. Spring AI 1.1.x gives you a first-class, annotation-driven path that feels exactly like the Spring you already know — no Python required.
Full Source Code
The complete project is available on GitHub: https://github.com/tanmoymandal/todo-mcp-server
Tags: #Java #SpringBoot #SpringAI #MCP #ModelContextProtocol #AI #LLM #Claude

