Skip to content

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:

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:

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:

application.properties
spring.ai.mcp.server.protocol=streamable
server.port=3001

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

  1. A Resource: The HTML/JavaScript UI that users interact with
  2. 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:

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:

DiceApp.java
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;
@Service
public 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 resource
  • uri: A unique identifier for the resource. The ui:// scheme indicates this is a UI resource
  • mimeType: Must be text/html;profile=mcp-app for MCP Apps
  • metaProvider: 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 users
  • name: The programmatic name used by the AI
  • description: What the tool does
  • metaProvider: Links additional metadata to this tool

Step 3: Create the Metadata Providers

The metaProvider classes link your tool to its resource:

DiceMetaProvider.java
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()
);
}
}
CspMetaProvider.java
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:

Terminal
./gradlew bootRun

Or with Maven:

Terminal
mvn spring-boot:run

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

  1. The AI calls the roll-the-dice tool
  2. The tool returns a message and metadata pointing to ui://dice/dice-app.html
  3. The MCP client fetches the HTML resource
  4. The user sees and interacts with the dice roller UI
  5. When the user rolls, the result is sent back to the AI through the ext-apps.ts communication 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.

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