A server that declares _meta.ui.csp.resourceDomains on its ui:// resource still cannot load images from those origins in claude.ai. The declaration appears to be dropped rather than evaluated, so every external image in the view renders as a broken-image glyph.
What the spec says
McpUiResourceCsp.resourceDomains is documented as mapping to img-src (among others), and McpUiSandboxProxyReadyNotification's sibling ui/notifications/sandbox-resource-ready defines a csp field on its params:
params: {
html: string;
sandbox?: string;
csp?: McpUiResourceCsp; // <- defined
permissions?: McpUiResourcePermissions;
}
registerAppResource's docs also say a resources/read content item's _meta.ui takes precedence over the listing-level value, implying both are read.
What we declared
On both the resource registration metadata and the resources/read result:
{ "_meta": { "ui": { "csp": { "resourceDomains": [
"https://storage.googleapis.com",
"https://firebasestorage.googleapis.com"
] } } } }
Verified on the wire by driving our published stdio server directly: both resources/list and the resources/read result carry it.
What we observed
The sandbox proxy document at https://.claudemcpcontent.com/mcp_apps?resource-src=... is served with a fixed response header:
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data;
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; ...
No server-declared origins appear in it.
The proxy builds the view's frame with document.write into an about:blank iframe, with an explanatory comment ("Use document.write() instead of srcdoc to preserve origin"). A document created that way inherits the creator's policy container, so the view runs under the same img-src.
The proxy's sandbox-resource-ready handler reads only two of the four defined params:
const {html, permissions} = data.params;
The string csp does not appear anywhere in that page, and neither does resourceDomains.
Reproduction of the effect
Serving a page with that exact header, building an about:blank iframe the same way, and writing one data: image and one https://storage.googleapis.com image into it:
data: image: renders, naturalWidth 1
https: image: blocked, naturalWidth 0
Console: Loading the image '' violates the following Content Security Policy directive: "img-src 'self' data: blob:". The action has been blocked.
Why this reading rather than "the host rejected our list"
Those two call for different fixes on our side. A rejection would mean our declaration is malformed. The absent handling in the proxy, plus a fixed header with no server-supplied origins, points at the field not being consumed at all.
We could be wrong about the mechanism: a text search over one bundle is not proof, the handling could live in another module, and minification can rename things. The blocked image and the console message are the observations; the rest is inference.
Impact
Any MCP App view that displays user content hosted outside the host's own origins cannot display it. The only workaround is inlining bytes as data: URIs in the tool result, which puts image data into the model's context window on every call and scales with the user's data.
Environment
@modelcontextprotocol/ext-apps 1.7.5
@modelcontextprotocol/sdk 1.30.0
claude.ai web, observed 2026-08-27
Resource mime text/html;profile=mcp-app
What would help
Either apply the declared resourceDomains to the inner frame's policy, or document that the field is not yet implemented on this host so servers do not ship a declaration that reads as a working guarantee.
A server that declares _meta.ui.csp.resourceDomains on its ui:// resource still cannot load images from those origins in claude.ai. The declaration appears to be dropped rather than evaluated, so every external image in the view renders as a broken-image glyph.
What the spec says
McpUiResourceCsp.resourceDomains is documented as mapping to img-src (among others), and McpUiSandboxProxyReadyNotification's sibling ui/notifications/sandbox-resource-ready defines a csp field on its params:
params: {
html: string;
sandbox?: string;
csp?: McpUiResourceCsp; // <- defined
permissions?: McpUiResourcePermissions;
}
registerAppResource's docs also say a resources/read content item's _meta.ui takes precedence over the listing-level value, implying both are read.
What we declared
On both the resource registration metadata and the resources/read result:
{ "_meta": { "ui": { "csp": { "resourceDomains": [
"https://storage.googleapis.com",
"https://firebasestorage.googleapis.com"
] } } } }
Verified on the wire by driving our published stdio server directly: both resources/list and the resources/read result carry it.
What we observed
The sandbox proxy document at https://.claudemcpcontent.com/mcp_apps?resource-src=... is served with a fixed response header:
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data;
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; ...
No server-declared origins appear in it.
The proxy builds the view's frame with document.write into an about:blank iframe, with an explanatory comment ("Use document.write() instead of srcdoc to preserve origin"). A document created that way inherits the creator's policy container, so the view runs under the same img-src.
The proxy's sandbox-resource-ready handler reads only two of the four defined params:
const {html, permissions} = data.params;
The string csp does not appear anywhere in that page, and neither does resourceDomains.
Reproduction of the effect
Serving a page with that exact header, building an about:blank iframe the same way, and writing one data: image and one https://storage.googleapis.com image into it:
data: image: renders, naturalWidth 1
https: image: blocked, naturalWidth 0
Console: Loading the image '' violates the following Content Security Policy directive: "img-src 'self' data: blob:". The action has been blocked.
Why this reading rather than "the host rejected our list"
Those two call for different fixes on our side. A rejection would mean our declaration is malformed. The absent handling in the proxy, plus a fixed header with no server-supplied origins, points at the field not being consumed at all.
We could be wrong about the mechanism: a text search over one bundle is not proof, the handling could live in another module, and minification can rename things. The blocked image and the console message are the observations; the rest is inference.
Impact
Any MCP App view that displays user content hosted outside the host's own origins cannot display it. The only workaround is inlining bytes as data: URIs in the tool result, which puts image data into the model's context window on every call and scales with the user's data.
Environment
@modelcontextprotocol/ext-apps 1.7.5
@modelcontextprotocol/sdk 1.30.0
claude.ai web, observed 2026-08-27
Resource mime text/html;profile=mcp-app
What would help
Either apply the declared resourceDomains to the inner frame's policy, or document that the field is not yet implemented on this host so servers do not ship a declaration that reads as a working guarantee.