modelcontextprotocol / modelcontextprotocol/ext-apps
Sandbox proxy does not apply _meta.ui.csp from the UI resource, so resourceDomains has no effect
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.9k
- Forks
- 387
- Avg merge
- 3h 21m
- Merged PRs (30d)
- 6
Description
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the external-image failure in claude.ai and tracing the sandbox-resource-ready handler described in the report, especially its handling of html, permissions, and csp. Compare the fixed proxy Content-Security-Policy with the declared resourceDomains; done means those origins affect the inner frame's image policy, or the unsupported behavior is documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100