MCP App Content Security Policy: Configuring CSP for External Scripts
I was building an MCP App and needed to load an external script from a CDN. I added the import statement, ran the app, and… nothing happened. The script failed to load silently.
Here’s what I tried initially:
<script type="module"> // This import failed silently</script>The browser console showed a CSP violation error. What was going on?
MCP Apps Run in a Sandbox
MCP Apps are designed with security in mind. They run in a sandboxed environment that blocks all external resources by default. This is intentional - it prevents malicious scripts from being loaded and protects the user.
But this also means you can’t load legitimate external libraries from CDNs without explicitly allowing them. The sandbox says “no external resources” unless you configure it otherwise.
What is Content Security Policy (CSP)?
CSP is a browser security feature that controls which resources a web page can load. Think of it as a whitelist. By default, if no CSP is set, browsers allow everything. But MCP Apps set a strict CSP that blocks external resources.
The CSP header tells the browser: “Only load scripts from these domains, only load styles from these domains, etc.” This is good security practice, but it means you need to configure CSP when you need external resources.
The Fix: Configure CSP via MetaProvider
To allow external resources, you need to set the Content Security Policy through the MetaProvider interface. This is done by setting the ui.csp.resourceDomains metadata entry to a list of allowed domains.
Here’s how I fixed it:
@McpResource(name = "My App Resource", uri = "ui://myapp/app.html", mimeType = "text/html;profile=mcp-app", metaProvider = CspMetaProvider.class) // Reference the CSP providerpublic String getAppResource() throws IOException { return appResource.getContentAsString(Charset.defaultCharset());}
// Define the CSP metadata providerpublic 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 key is the metaProvider = CspMetaProvider.class parameter. This tells the MCP framework to use my custom MetaProvider to get metadata for this resource.
Inside CspMetaProvider, I return a map with the CSP configuration:
ui.csp.resourceDomains -> ["https://unpkg.com"]This tells the MCP runtime: “Allow resources from unpkg.com.”
Allowing Multiple Domains
If you need to load resources from multiple CDNs, just add them to the list:
@Overridepublic Map<String, Object> getMeta() { return Map.of("ui", Map.of("csp", Map.of("resourceDomains", List.of( "https://unpkg.com", "https://cdn.jsdelivr.net", "https://fonts.googleapis.com" ))));}Each domain in the list gets added to the CSP whitelist.
Why This Approach Works
The MetaProvider interface lets you attach metadata to MCP resources. The MCP runtime reads this metadata when serving the resource and generates the appropriate CSP headers.
When the browser receives the HTML, it also receives a CSP header like:
Content-Security-Policy: default-src 'self'; script-src 'self' https://unpkg.com; ...Now the browser knows it’s okay to load scripts from unpkg.com.
Common Mistakes
I made a few mistakes along the way:
-
Forgetting the
metaProviderparameter: I defined theCspMetaProviderclass but forgot to reference it in the@McpResourceannotation. The CSP wasn’t applied. -
Wrong domain format: I used
unpkg.cominstead ofhttps://unpkg.com. The protocol matters - include it. -
Catching the error too late: The import failed silently in my app. I should have checked the browser console immediately to see the CSP violation error.
Summary
When MCP Apps need to load external resources:
- MCP Apps are sandboxed and block external resources by default
- Use
MetaProviderto configure CSP - Set
ui.csp.resourceDomainsto a list of allowed domains - Reference your provider in
@McpResource(metaProvider = ...) - Always check the browser console for CSP errors
The sandbox is there for security. Don’t disable it entirely - just allow the specific domains you need.
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