How to Use MetaProvider in Spring AI for MCP App Metadata
I was building an MCP App in Spring AI, and everything seemed fine until I tried to load an external script from unpkg.com. The console screamed:
Refused to load the script 'https://unpkg.com/[email protected]'because it violates the following Content Security Policy directive:"default-src 'self'"My MCP App wouldn’t work. The UI was broken. What was going on?
The Sandbox Problem
MCP Apps in Spring AI run in a sandboxed environment by default. This is a security feature, not a bug. But it means:
- External resources are blocked — CDN scripts, stylesheets, and fonts won’t load
- Tools don’t know their UI resources — When a tool is invoked, the client needs metadata to know which HTML resource to display
I needed a way to:
- Tell the browser it’s okay to load scripts from
unpkg.com - Link my
@McpToolto the HTML resource defined via@McpResource
Discovering MetaProvider
I dug through the Spring AI MCP documentation and found MetaProvider.
public interface MetaProvider { Map<String, Object> getMeta();}It’s a simple functional interface that returns metadata as a Map<String, Object>. The metadata keys Spring AI recognizes:
| Metadata Key | Purpose | Used On |
|---|---|---|
ui.resourceUri | Links a tool to its HTML resource | @McpTool |
ui.csp.resourceDomains | Whitelists external domains for CSP | @McpResource |
First Attempt: Setting CSP on @McpResource
I created a CspMetaProvider to allow scripts from unpkg.com:
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")))); }}Then I referenced it in my @McpResource annotation:
@McpResource( name = "My App Resource", uri = "ui://myapp/app.html", mimeType = "text/html;profile=mcp-app", metaProvider = CspMetaProvider.class // Reference the CLASS, not instance)public String getAppResource() throws IOException { ClassPathResource resource = new ClassPathResource("templates/myapp/app.html"); return resource.getContentAsString(Charset.defaultCharset());}Key insight: Use metaProvider = CspMetaProvider.class — you reference the class, not an instance. Spring AI instantiates it.
The Metadata Structure
The tricky part was the nested map structure. Let me break it down:
+-- "ui" (root key)| +-- "csp" (nested under ui)| | +-- "resourceDomains" (list of domain strings)| | | +-- "https://unpkg.com"| | | +-- "https://cdn.jsdelivr.net"For ui.resourceUri, the structure is:
+-- "ui" (root key)| +-- "resourceUri" (string value)| | +-- "ui://myapp/app.html"Second Attempt: Linking Tool to Resource
Now I needed to link my @McpTool to the HTML resource. I created another MetaProvider:
public static final class DiceMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of("ui", Map.of("resourceUri", "ui://myapp/dice.html")); }}And used it on my tool:
@McpTool( title = "Roll Dice", name = "roll-dice", description = "Rolls an interactive 3D dice", metaProvider = DiceMetaProvider.class)public String rollDice() { return "Dice rolled!";}Now when the tool is invoked, the client knows to display ui://myapp/dice.html.
Common Mistakes I Made
1. Setting CSP on @McpTool instead of @McpResource
@McpTool( name = "my-tool", metaProvider = CspMetaProvider.class // Wrong! CSP belongs on @McpResource)CSP metadata should be on the @McpResource that serves the HTML, not on the tool.
2. Using Wrong Metadata Structure
return Map.of("ui.resourceUri", "ui://myapp/app.html"); // Wrong!The correct structure is nested:
return Map.of("ui", Map.of("resourceUri", "ui://myapp/app.html"));3. Forgetting Protocol in Domain URLs
List.of("unpkg.com") // Wrong! Browser won't allow thisAlways include the protocol:
List.of("https://unpkg.com")4. Not Using Static Inner Class
public class CspMetaProvider implements MetaProvider { // Wrong if not static @Override public Map<String, Object> getMeta() { // ... }}If you define it as an inner class, make it static final:
public static final class CspMetaProvider implements MetaProvider { // ...}Complete Example: Dice App with MetaProvider
Here’s a complete working example:
package com.example.mcp;
import org.springframework.ai.mcp.server.McpTool;import org.springframework.ai.mcp.server.McpResource;import org.springframework.ai.mcp.server.MetaProvider;import org.springframework.core.io.ClassPathResource;
import java.io.IOException;import java.nio.charset.Charset;import java.util.List;import java.util.Map;
public class DiceMcpApp {
@McpResource( name = "Dice App Resource", uri = "ui://dice/app.html", mimeType = "text/html;profile=mcp-app", metaProvider = DiceCspMetaProvider.class ) public String getDiceResource() throws IOException { ClassPathResource resource = new ClassPathResource("templates/dice/app.html"); return resource.getContentAsString(Charset.defaultCharset()); }
@McpTool( title = "Roll Dice", name = "roll-dice", description = "Rolls an interactive 3D dice", metaProvider = DiceResourceUriMetaProvider.class ) public String rollDice() { return "Dice rolled! Click to see the result."; }
// CSP for the HTML resource public static final class DiceCspMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of("ui", Map.of("csp", Map.of("resourceDomains", List.of( "https://unpkg.com", "https://cdn.jsdelivr.net" )))); } }
// Link tool to resource public static final class DiceResourceUriMetaProvider implements MetaProvider { @Override public Map<String, Object> getMeta() { return Map.of("ui", Map.of("resourceUri", "ui://dice/app.html")); } }}How It Works Together
+-------------------+ +---------------------+ +------------------+| @McpResource | | MetaProvider | | Browser/Client || uri: ui://dice |---->| ui.csp. |---->| Allows scripts || metaProvider: | | resourceDomains: | | from unpkg.com || DiceCspMeta | | [unpkg.com] | | |+-------------------+ +---------------------+ +------------------+
+-------------------+ +---------------------+ +------------------+| @McpTool | | MetaProvider | | Client displays || name: roll-dice |---->| ui.resourceUri: |---->| dice.html when || metaProvider: | | ui://dice/app.html | | tool invoked || DiceResourceUri | | | | |+-------------------+ +---------------------+ +------------------+Why This Design?
Security by default: MCP Apps start in a locked-down sandbox. You must explicitly opt-in to external resources.
Explicit over implicit: The MetaProvider pattern makes you think about which domains you’re whitelisting.
Separation of concerns: CSP goes on the resource, resource URI goes on the tool. Each piece has its job.
Related Patterns
If you’re building MCP Apps, you might also need:
- Tool annotations — Use
@McpToolattributes for title, name, description - Resource annotations — Use
@McpResourcefor serving HTML templates - MCP Server configuration — Configure the MCP server endpoint in your Spring Boot app
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!
References
- Spring AI MCP Documentation: https://docs.spring.io/spring-ai/reference/api/mcp.html
- Model Context Protocol (MCP) Specification: https://modelcontextprotocol.io/
- Content Security Policy (CSP) on MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Comments