@McpTool and @McpResource Annotations in Spring AI: Complete Reference
I was building an MCP server with Spring AI and hit a wall. I had created an HTML UI for my MCP App, and I wanted to link it to a tool so that when users called the tool, the UI would appear. But I couldn’t figure out how to connect them. The tool annotation only had name and description attributes. Where was the metadata?
Turns out, Spring AI has a metaProvider attribute that I completely missed. Let me show you how to use @McpTool and @McpResource annotations properly, including the metadata part that ties everything together.
The Problem: Missing Metadata
When you define an MCP tool, the basics are straightforward:
@McpTool(name = "my-tool", description = "Does something useful")public String doSomething() { return "Done!";}This works fine for simple tools. But when you want to build MCP Apps with interactive UIs, you need metadata. The MCP client needs to know:
- Which HTML resource to load for your tool’s UI
- What content security policies to apply
- Any other custom metadata for your app
Standard annotation attributes don’t cover this. You need something extensible.
The Solution: MetaProvider Interface
Spring AI solves this with the MetaProvider interface. It’s simple:
public interface MetaProvider { Map<String, Object> getMeta();}You implement this interface and return whatever metadata you need. Then you reference it from your annotations via the metaProvider attribute.
@McpResource: Serving Static Content
Let me start with @McpResource. This annotation registers a method that serves static content at a custom URI. It’s useful for HTML, JSON, or any static content your MCP server provides.
Basic @McpResource Usage
@Servicepublic class ResourceProvider {
@McpResource( name = "My App Resource", uri = "ui://myapp/index.html", mimeType = "text/html" ) public String getResource() { return "<html><body>Hello from MCP!</body></html>"; }}The uri is the identifier clients use to request this resource. The mimeType tells them what type of content to expect.
MCP App MIME Type
Here’s where I made my first mistake. For MCP Apps, you need a special MIME type:
@McpResource( name = "Dice App Resource", uri = "ui://dice/dice-app.html", mimeType = "text/html;profile=mcp-app" // The profile=mcp-app part is critical!)public String getDiceAppResource() throws IOException { return diceAppResource.getContentAsString(Charset.defaultCharset());}The profile=mcp-app in the MIME type signals to MCP clients that this resource is an interactive app UI, not just a static HTML page. Without it, the client might treat it as plain HTML without the MCP App integration.
Adding CSP Metadata with MetaProvider
My second mistake was forgetting Content Security Policy. When your app loads external scripts (like from unpkg.com), you need to allow those domains. This is where MetaProvider comes in.
@Servicepublic class DiceAppService {
private final Resource diceAppResource;
public DiceAppService(@Value("classpath:dice-app.html") Resource diceAppResource) { this.diceAppResource = 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()); }
// Inner class for metadata public static final class CspMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of("ui", Map.of("csp", Map.of("resourceDomains", List.of("https://unpkg.com")))); } }}The metadata structure follows MCP’s specification. For CSP, it’s:
ui.csp.resourceDomains -> List of allowed domainsThis tells the MCP client that your app can load scripts from https://unpkg.com. Without this, external scripts would be blocked.
@McpTool: Defining Callable Functions
Now let’s look at @McpTool. This annotation registers a method as an MCP tool that AI models can call.
Basic @McpTool Usage
@Servicepublic class DiceTool {
@McpTool( title = "Roll the Dice", name = "roll-the-dice", description = "Rolls a six-sided die and returns the result" ) public String rollTheDice() { int result = (int) (Math.random() * 6) + 1; return "You rolled a " + result + "!"; }}The attributes:
title- Human-readable name shown to usersname- Programmatic identifier (kebab-case recommended)description- What the tool does (shown to AI models)
Linking Tools to UI Resources
Here’s the key part: linking your tool to its UI resource. You use MetaProvider for this too.
@Servicepublic class DiceTool {
@McpTool( title = "Roll the Dice", name = "roll-the-dice", description = "Opens an interactive dice roller app", 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")); } }}The ui.resourceUri metadata tells the MCP client: “When this tool is called, load the UI from this URI.” The URI matches what you defined in your @McpResource.
Putting It All Together
Here’s a complete example with both tool and resource:
@Servicepublic class DiceApp {
private final Resource diceAppResource;
public DiceApp(@Value("classpath:dice-app.html") Resource diceAppResource) { this.diceAppResource = diceAppResource; }
// The tool that triggers the UI @McpTool( title = "Roll the Dice", name = "roll-the-dice", description = "Rolls the dice", metaProvider = DiceMetaProvider.class ) public String rollTheDice() { return "Opening dice roller app."; }
// The HTML resource served to the client @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()); }
// Tool metadata: links to the UI resource 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")); } }
// Resource metadata: CSP configuration public static final class CspMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of("ui", Map.of("csp", Map.of("resourceDomains", List.of("https://unpkg.com")))); } }}Common Mistakes I Made
Mistake 1: Forgetting the MCP App Profile
I initially used mimeType = "text/html" instead of mimeType = "text/html;profile=mcp-app". The client loaded my HTML but didn’t treat it as an MCP App. The UI didn’t connect to the AI assistant.
Fix: Always include profile=mcp-app for interactive UIs.
Mistake 2: Missing CSP Configuration
My dice app loaded the MCP App library from unpkg.com:
<script type="module"> // ...</script>But without CSP metadata, the browser blocked the external script. The app failed silently.
Fix: Add resourceDomains metadata for every external domain your app uses.
Mistake 3: URI Mismatch
I had a typo in my URI. The tool referenced ui://dice/app.html but the resource was registered at ui://dice/dice-app.html. The client couldn’t find the resource.
Fix: Double-check that your tool’s resourceUri exactly matches your resource’s uri.
Mistake 4: MetaProvider Class Not Public
I made my MetaProvider class package-private initially. Spring couldn’t instantiate it.
Fix: Make MetaProvider classes public static (if inner classes) or just public (if top-level classes).
Annotation Attributes Reference
@McpTool Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
title | String | No | Human-readable name |
name | String | No | Programmatic identifier |
description | String | No | Tool description for AI |
metaProvider | Class<? extends MetaProvider> | No | Custom metadata provider |
@McpResource Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
name | String | No | Resource name |
uri | String | No | Resource URI identifier |
mimeType | String | No | Content MIME type |
metaProvider | Class<? extends MetaProvider> | No | Custom metadata provider |
When to Use Which
Use @McpTool when:
- You want AI models to call a function
- The function might have a UI associated with it
- You need to expose business logic to AI
Use @McpResource when:
- You need to serve static content (HTML, JSON, etc.)
- You’re building an MCP App UI
- You want to provide assets at custom URIs
Use both together when:
- Building an MCP App with a tool trigger
- The tool initiates an action, the resource provides the UI
Spring Configuration
Don’t forget to enable MCP in your Spring Boot application:
@Configuration@EnableMcppublic class McpConfig { // MCP server configuration}And in your application.properties or application.yml:
spring: ai: mcp: server: enabled: true name: my-mcp-server version: 1.0.0Wrapping Up
The metaProvider attribute in @McpTool and @McpResource is the bridge between simple annotations and rich MCP App experiences. It follows Spring’s pattern of keeping annotation attributes minimal while allowing extensibility through separate interfaces.
The key takeaways:
- Use
profile=mcp-appin MIME types for MCP App UIs - MetaProvider returns a Map for custom metadata
- Tool metadata uses
ui.resourceUrito link to resources - Resource metadata uses
ui.csp.resourceDomainsfor security policies - Keep URIs consistent between tool references and resource definitions
Once I understood this pattern, building MCP Apps with Spring AI became straightforward. The annotations handle the MCP protocol details, and I focus on the UI and business logic.
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