How to Create MCP Apps with Spring AI: Embed Rich UIs in Chat Interfaces
The Problem: Text-Only Chat Is Limiting
I was building an AI assistant that needed to let users select locations on a map. Describing coordinates in natural language is clumsy: “I want the location around latitude 37.7749, longitude -122.4194” — not ideal. What I really needed was an interactive map UI embedded directly in the chat.
Traditional chat interfaces only support text-based interactions. While natural language is flexible, some actions are better served by visual UIs — clicking a map location is more precise than describing coordinates, selecting dates from a calendar is easier than typing them, and choosing from a list is faster than describing preferences.
This is where MCP Apps come in.
What Are MCP Apps?
MCP Apps let you embed rich HTML/JavaScript UIs directly in AI chat interfaces. When the AI assistant needs user input that’s better handled visually, it can trigger an MCP App that renders an interactive UI within the chat context.
The architecture looks like this:
+------------------+ +------------------+ +------------------+| AI Assistant |---->| MCP Server |---->| HTML/JS UI || (Chat Client) | | (Spring AI) | | (MCP App) |+------------------+ +------------------+ +------------------+ | | | | 1. Call @McpTool | | |------------------------>| | | | 2. Return metadata | | | with resourceUri | | |<-------------------------| | | | | 3. Fetch HTML from | | | resourceUri | | |------------------------>| | | | 4. Return HTML/CSS/JS | | |<-------------------------| | | | 5. Render UI in chat context | |---------------------------------------------------| | | | 6. User interacts with UI | |---------------------------------------------------| | | | 7. UI sends results back via ext-apps.ts | |<---------------------------------------------------|An MCP App has two primary components:
- Tool method (
@McpTool): Triggers the app, provides metadata linking to the HTML resource - Resource method (
@McpResource): Serves the HTML/CSS/JavaScript UI
My First Attempt: Missing Version Requirement
I started with Spring AI 1.0.0-M4 and tried to use @McpTool annotation:
@Servicepublic class DiceApp {
@McpTool(name = "roll-the-dice", description = "Rolls the dice") public String rollTheDice() { return "Opening dice roller app."; }}But the annotation wasn’t recognized:
error: cannot find symbol @McpTool(name = "roll-the-dice", description = "Rolls the dice") ^ symbol: class McpToolThe fix: MCP annotations require Spring AI 2.0.0-M3 or later. I updated my pom.xml:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId> <version>2.0.0-M3</version></dependency>Creating the MCP Resource (HTML UI)
First, I created the HTML resource that will be served by the MCP server. This is the interactive UI that appears in the chat:
<!DOCTYPE html><html><head> <title>Dice Roller</title> <style> body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } .dice { font-size: 48px; margin: 20px 0; } button { padding: 10px 20px; font-size: 16px; cursor: pointer; } </style></head><body> <h2>Roll the Dice</h2> <div class="dice" id="result">Click to roll!</div> <button onclick="rollDice()">Roll</button>
<script type="module">
const app = new App({ name: "roll-the-dice", version: "1.0.0" });
// Connect to the assistant host await app.connect();
window.rollDice = async function() { const die1 = Math.floor(Math.random() * 6) + 1; const die2 = Math.floor(Math.random() * 6) + 1; const result = `${die1} and ${die2}`;
document.getElementById('result').textContent = result;
// Send the result back to the AI assistant await app.updateModelContext({ content: [{ type: "text", text: `Dice roll: ${result}` }], }); }; </script></body></html>Key points about this HTML:
- Uses
ext-apps.tsmodule from CDN for communication with the AI host - Calls
app.connect()before any UI interactions - Uses
app.updateModelContext()to send results back to the assistant
Serving the HTML with @McpResource
Now I needed to serve this HTML from the MCP server using the @McpResource annotation:
package com.example.mcp;
import org.springframework.ai.tool.annotation.McpResource;import org.springframework.ai.tool.annotation.providers.CspMetaProvider;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;import java.util.Map;
@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()); }}Important details:
urimust use a custom scheme likeui://— this is how the client knows where to fetch the HTMLmimeTypemust betext/html;profile=mcp-app— this signals it’s an MCP App, not regular HTMLmetaProvider = CspMetaProvider.classsets Content Security Policy headers for external resources
The Second Problem: How Does the Tool Know About the Resource?
I had the HTML resource, but how does the AI assistant know to trigger it? I needed a tool that references the resource:
@Servicepublic class DiceApp {
// ... resource method from above ...
@McpTool( title = "Roll the Dice", name = "roll-the-dice", description = "Rolls virtual dice and returns the result", metaProvider = DiceMetaProvider.class ) public String rollTheDice() { return "Opening dice roller app."; }}But I was missing a critical piece: the MetaProvider that links the tool to the HTML resource.
Creating the MetaProvider
The MetaProvider is the bridge between the tool and the resource. It provides metadata that tells the AI client where to find the HTML:
package com.example.mcp;
import org.springframework.ai.tool.annotation.MetaProvider;import java.util.Map;
public static final class DiceMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of( "ui", Map.of( "resourceUri", "ui://dice/dice-app.html" ) ); }}This metadata tells the AI client: “When this tool is invoked, fetch the HTML from ui://dice/dice-app.html and render it inline.”
The Complete Solution
Here’s the complete working implementation:
package com.example.mcp;
import org.springframework.ai.tool.annotation.McpResource;import org.springframework.ai.tool.annotation.McpTool;import org.springframework.ai.tool.annotation.MetaProvider;import org.springframework.ai.tool.annotation.providers.CspMetaProvider;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;import java.util.Map;
@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 virtual dice and returns the result", metaProvider = DiceMetaProvider.class ) public String rollTheDice() { return "Opening dice roller app."; }
public static final class DiceMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of( "ui", Map.of( "resourceUri", "ui://dice/dice-app.html" ) ); } }}Bidirectional Communication
The real power of MCP Apps is bidirectional communication. The UI can:
Update the model context:
await app.updateModelContext({ content: [{ type: "text", text: "Dice roll: 4 and 6" }],});Call server-side tools:
const result = await app.callServerTool("analyze-dice-roll", { roll: [4, 6]});Send messages to the conversation:
await app.sendMessage({ role: "user", content: [{ type: "text", text: "I rolled a 4 and 6!" }],});This creates a hybrid interaction model: users get the flexibility of natural language chat combined with the precision of traditional UIs — all within the same conversation context. The model stays aware of UI state through context updates.
Common Mistakes to Avoid
Mistake 1: Wrong Spring AI Version
<!-- WRONG: Too old, no MCP annotations --><version>1.0.0-M4</version>
<!-- CORRECT: MCP annotations available --><version>2.0.0-M3</version>Mistake 2: Missing CSP Metadata
// WRONG: No CSP provider, external scripts blocked@McpResource(name = "App", uri = "ui://app.html")
// CORRECT: Include CSP metadata for external resources@McpResource( name = "App", uri = "ui://app.html", metaProvider = CspMetaProvider.class)Mistake 3: Forgetting to Connect
// WRONG: Calling updateModelContext before connectconst app = new App({ name: "my-app" });await app.updateModelContext({...}); // Fails silently
// CORRECT: Connect firstconst app = new App({ name: "my-app" });await app.connect();await app.updateModelContext({...}); // WorksMistake 4: Wrong MIME Type
// WRONG: Regular HTML, not recognized as MCP AppmimeType = "text/html"
// CORRECT: Signals MCP App to clientmimeType = "text/html;profile=mcp-app"Transport Protocol Considerations
Different AI clients support different transport protocols:
| Transport | Use Case | Client Support |
|---|---|---|
stdio | Local development, CLI tools | Most clients |
SSE (Server-Sent Events) | Web-based clients | Claude Desktop, Cursor |
| HTTP with SSE | Production deployments | Varies by client |
For web deployment, configure the transport:
spring: ai: mcp: server: type: sse sse-message-endpoint: /mcp/messageSummary
MCP Apps with Spring AI bridge the gap between chat flexibility and UI precision. Here’s what I learned:
- Use
@McpResourceto serve HTML withmimeType = "text/html;profile=mcp-app" - Use
@McpToolwith aMetaProviderthat setsui.resourceUrito link the tool to the resource - Include
CspMetaProvider.classwhen loading external scripts or styles - Use
ext-apps.tsmodule for bidirectional communication (updateModelContext,callServerTool,sendMessage) - Always call
app.connect()before UI interactions - Spring AI 2.0.0-M3+ is required for MCP annotations
The hybrid interaction model — natural language when it makes sense, visual UIs when they’re better — all within the same chat context — makes MCP Apps a powerful tool for building sophisticated AI assistants.
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