Skip to main content

Command Palette

Search for a command to run...

Build a Production-Ready MCP Server with Spring Boot 4 & Spring AI 1.1

Published
9 min read

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:

  1. Recognizes it needs external data

  2. Calls your getTodosByPriority tool with priority="CRITICAL"

  3. Reads the JSON response

  4. 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: TodoService has zero dependency on Spring AI. The TodoMcpTools class 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 /sse and /mcp/messages endpoints for you. The alternative spring-ai-starter-mcp-server is STDIO only (suitable for local subprocess invocation). Use webmvc for 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/sse

  • H2 Console: http://localhost:8080/h2-console

  • Health 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, @Transactional

  • Keeps 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

Artificial Intelligence

Part 2 of 3

Exploring the frontiers of Artificial Intelligence. From deep learning breakthroughs to practical implementation guides, this series breaks down the complex world of AI into actionable insights for developers and enthusiasts alike.

Up next

The Ultimate AI Glossary: 60+ Terms Every Developer Should Know in 2026

From Transformers to RAG, Agents to Embeddings — decoded. Whether you're diving into your first machine learning project or architecting enterprise AI systems, the landscape of AI terminology can fee