Skip to content

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

BasicTool.java
@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:

  1. Which HTML resource to load for your tool’s UI
  2. What content security policies to apply
  3. 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:

MetaProvider.java
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

ResourceProvider.java
@Service
public 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:

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

DiceAppWithCsp.java
@Service
public 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 domains

This 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

SimpleTool.java
@Service
public 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 users
  • name - 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.

DiceToolWithUi.java
@Service
public 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:

CompleteDiceApp.java
@Service
public 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">
import { App } from "https://unpkg.com/@modelcontextprotocol/[email protected]/app-with-deps";
// ...
</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

AttributeTypeRequiredDescription
titleStringNoHuman-readable name
nameStringNoProgrammatic identifier
descriptionStringNoTool description for AI
metaProviderClass<? extends MetaProvider>NoCustom metadata provider

@McpResource Attributes

AttributeTypeRequiredDescription
nameStringNoResource name
uriStringNoResource URI identifier
mimeTypeStringNoContent MIME type
metaProviderClass<? extends MetaProvider>NoCustom 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:

McpConfig.java
@Configuration
@EnableMcp
public class McpConfig {
// MCP server configuration
}

And in your application.properties or application.yml:

application.yml
spring:
ai:
mcp:
server:
enabled: true
name: my-mcp-server
version: 1.0.0

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

  1. Use profile=mcp-app in MIME types for MCP App UIs
  2. MetaProvider returns a Map for custom metadata
  3. Tool metadata uses ui.resourceUri to link to resources
  4. Resource metadata uses ui.csp.resourceDomains for security policies
  5. 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