How to Build MCP Apps with Spring AI 2.0: Step-by-Step Tutorial
I wanted to add rich UIs to my AI chat applications, but I kept running into the same problem: there was no clean, structured way to integrate interactive interfaces with AI tools. Then I discovered MCP Apps in Spring AI 2.0, and everything clicked into place.
The Problem I Was Trying to Solve
When building AI-powered applications, I often needed more than just text responses. I wanted dice rollers, form inputs, interactive visualizations - all within the chat interface. But connecting these UIs to my backend tools felt messy and disconnected.
The Model Context Protocol (MCP) provides a way for AI assistants to interact with external tools and resources. Spring AI 2.0.0-M3 took this further by adding support for MCP Apps - a way to serve rich HTML interfaces alongside your tools.
Getting Started: What You Need
Before diving in, make sure you have:
- Java 17 or higher
- Spring Boot 3.x
- Spring AI 2.0.0-M3 or later (this is critical - earlier versions don’t have MCP annotations)
I made the mistake of using an older Spring AI version at first. The @McpTool and @McpResource annotations simply weren’t there, and I spent hours debugging why my code wouldn’t compile. Don’t repeat my mistake.
Setting Up Your Project
Gradle Configuration
Create a new Spring Boot project and add the Spring AI version to your build.gradle:
ext { set('springAiVersion', "2.0.0-M3")}
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.ai:spring-ai-mcp-server-webmvc-spring-boot-starter'}Maven Configuration
If you prefer Maven, update your pom.xml:
<properties> <spring-ai.version>2.0.0-M3</spring-ai.version></properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId> </dependency></dependencies>Application Properties
Add these configuration properties to application.properties:
spring.ai.mcp.server.protocol=streamableserver.port=3001The streamable protocol is important. It allows the MCP server to handle HTTP streaming, which is essential for the responsive UI experience. I initially forgot this setting and wondered why my app felt sluggish.
Creating Your First MCP App
Let me show you how to build a simple dice roller app. This example will help you understand the core concepts.
Understanding the Structure
An MCP App has two main parts:
- A Resource: The HTML/JavaScript UI that users interact with
- A Tool: The backend capability that the AI can invoke
These are connected through metadata. When the AI calls the tool, it also receives information about where to find the UI.
Step 1: Create the HTML Resource
Create a file at src/main/resources/app/dice-app.html:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dice Roller</title> <style> body { font-family: system-ui, sans-serif; max-width: 400px; margin: 2rem auto; padding: 1rem; } .dice-display { font-size: 4rem; text-align: center; margin: 1rem 0; } button { width: 100%; padding: 1rem; font-size: 1.2rem; cursor: pointer; } </style></head><body> <h1>Dice Roller</h1> <div class="dice-display" id="result">?</div> <button onclick="rollDice()">Roll the Dice</button>
<script type="module"> import { McpApp } from './ext-apps.ts';
const app = new McpApp();
async function rollDice() { const result = Math.floor(Math.random() * 6) + 1; document.getElementById('result').textContent = result;
// Send result back to the AI await app.sendMessage({ type: 'dice-result', value: result }); }
window.rollDice = rollDice; </script></body></html>The key here is the ext-apps.ts module. This is provided by Spring AI and handles communication between your HTML app and the AI assistant. It lets you send messages back and forth, creating a two-way interaction channel.
Step 2: Create the MCP Server Service
Now create the Java service that exposes both the tool and resource:
package com.example.mcpapp;
import org.springframework.ai.mcp.server.McpTool;import org.springframework.ai.mcp.server.McpResource;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Service;
import java.io.IOException;import java.nio.charset.Charset;
@Servicepublic class DiceApp {
@Value("classpath:/app/dice-app.html") private Resource diceAppResource;
@McpResource( name = "Dice App Resource", uri = "ui://dice/dice-app.html", mimeType = "text/html;profile=mcp-app", metaProvider = CspMetaProvider.class ) public String getDiceAppResource() throws IOException { return diceAppResource.getContentAsString(Charset.defaultCharset()); }
@McpTool( title = "Roll the Dice", name = "roll-the-dice", description = "Rolls the dice and opens an interactive dice roller app", metaProvider = DiceMetaProvider.class ) public String rollTheDice() { return "Opening dice roller app. The user can now interact with the dice roller."; }}Let me break down what’s happening here.
Understanding the Annotations
@McpResource
This annotation tells Spring AI that this method provides an MCP resource. The key attributes are:
name: A human-readable name for the resourceuri: A unique identifier for the resource. Theui://scheme indicates this is a UI resourcemimeType: Must betext/html;profile=mcp-appfor MCP AppsmetaProvider: A class that provides additional metadata (we’ll cover this next)
@McpTool
This annotation exposes a method as an MCP tool. The attributes:
title: The display name shown to usersname: The programmatic name used by the AIdescription: What the tool doesmetaProvider: Links additional metadata to this tool
Step 3: Create the Metadata Providers
The metaProvider classes link your tool to its resource:
package com.example.mcpapp;
import org.springframework.ai.mcp.server.MetaProvider;import org.springframework.ai.mcp.server.ResourceMetadata;
import java.util.List;
public class DiceMetaProvider implements MetaProvider {
@Override public List<ResourceMetadata> getResourceMetadata() { return List.of( ResourceMetadata.builder() .uri("ui://dice/dice-app.html") .type("interactive") .build() ); }}package com.example.mcpapp;
import org.springframework.ai.mcp.server.MetaProvider;import org.springframework.ai.mcp.server.CspPolicy;
import java.util.List;
public class CspMetaProvider implements MetaProvider {
@Override public List<CspPolicy> getCspPolicies() { return List.of( CspPolicy.builder() .defaultSrc("'self'") .scriptSrc("'self' 'unsafe-inline'") .styleSrc("'self' 'unsafe-inline'") .build() ); }}The DiceMetaProvider tells the AI that when this tool is invoked, it should also show the UI at ui://dice/dice-app.html. The CspMetaProvider defines Content Security Policy rules for the HTML resource - important for security in production environments.
Running Your MCP App
Start your Spring Boot application:
./gradlew bootRunOr with Maven:
mvn spring-boot:runYour MCP server will be available at http://localhost:3001. You can now connect an MCP client (like Claude Desktop or another AI assistant) to interact with your dice roller.
How It All Works Together
When an AI assistant wants to roll dice:
- The AI calls the
roll-the-dicetool - The tool returns a message and metadata pointing to
ui://dice/dice-app.html - The MCP client fetches the HTML resource
- The user sees and interacts with the dice roller UI
- When the user rolls, the result is sent back to the AI through the
ext-apps.tscommunication channel
This creates a seamless experience where the AI can provide both text responses and interactive UIs.
Common Mistakes I Made
Wrong Spring AI Version
I initially used Spring AI 1.x. The MCP annotations weren’t available, and nothing worked. Always check you’re using 2.0.0-M3 or later.
Missing Streamable Protocol
Forgetting spring.ai.mcp.server.protocol=streamable caused issues with the HTTP transport. The UI would load but interactions felt disconnected.
Incorrect MIME Type
I tried using just text/html for the MIME type. The profile=mcp-app suffix is essential - it tells the MCP client this is an interactive app, not just a static page.
Forgetting the Resource Metadata
The connection between tool and resource happens through the metaProvider. Without it, the AI calls your tool but has no idea where to find the UI.
Going Further: Building More Complex Apps
Once you understand the basics, you can build more sophisticated applications:
- Form inputs: Collect structured data from users
- Charts and visualizations: Display data the AI processes
- Multi-step wizards: Guide users through complex workflows
- Real-time updates: Show live data alongside AI responses
The key is keeping the UI focused and the communication clear between your HTML and the AI through the ext-apps.ts module.
Related Knowledge
Model Context Protocol (MCP)
MCP is an open protocol that standardizes how AI assistants connect to external systems. Think of it like USB for AI - one standard way to plug in tools, resources, and prompts. Spring AI’s MCP support means you can build servers that work with any MCP-compliant client.
Why Spring AI?
Spring AI provides a familiar Spring Boot programming model for building AI applications. Instead of learning MCP’s low-level details, you use annotations and dependency injection - patterns you already know. The framework handles the protocol complexity, letting you focus on your application logic.
Security Considerations
The Content Security Policy (CSP) metadata is crucial for production apps. It restricts what resources your HTML can load, preventing XSS attacks and other vulnerabilities. Always define appropriate CSP rules for your use case.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments