# 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.

```plaintext
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

```plaintext
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.

```xml
<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`

```yaml
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

```java
@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.

```java
@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

```java
@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

```bash
./mvnw spring-boot:run
```

You'll see in the logs:

```plaintext
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`:

```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:

```java
@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`
