<iframe> inside its view. A view is one rendered instance of your app’s UI in Claude. For example, you can show your product’s own editor or dashboard in a view.
This page is for developers who have a working MCP App in Claude, served by a remote MCP server.
These cases are covered on other pages:
- Opening a site in a new tab instead: see Open external links from MCP Apps
- Building your first MCP App: see Add an interactive UI to your MCP server
- Where sites load: Claude applies different rules to directory connectors and custom connectors, so start with where embedded sites load
- Device features: check which device features your connector’s views lose, such as the camera
Where embedded sites load
Claude has to allow a site’s origin for your connector before the site loads.- Directory connector: a connector that has a listing in the Connectors Directory
- Custom connector: a connector that a user adds to Claude by URL
Allowed origins in a directory connector
For a directory connector, the Verified listing has to allow each origin. If you declare a new origin, it can’t load until the listing allows it as well. For how a listing becomes Verified, see Connector verification. When Claude blocks an origin for a directory connector, the view opens and the<iframe> doesn’t show the site. Claude shows no message, so show a fallback in the view.
Allowed origins in a custom connector
Where embedded sites are available for custom connectors, Claude lists the origins that you declare for the view and asks the user to allow them. For embedded sites, a connector counts as custom when a user added it by URL and its address matches no directory listing. Check these details of your connector:- Characters in the path: use a lower-case path without
%,;, or//. If the path contains a capital letter or one of those characters, the connector doesn’t count as custom, and Claude blocks its origins. - Query string: to test a new build of a listed connector, serve the build at its own address. Claude doesn’t use the query string when it matches an address to a listing.
- Server URL that matches a listing: Claude treats the connector as that directory connector. A site that loaded while you tested is blocked under a Community listing.
- Connector added many months ago: the connector may not count as custom. If Claude doesn’t ask you to allow the origins, remove the connector and add it again.
Availability of embedded sites for custom connectors
For custom connectors, embedded sites are available to a limited set of organizations, and not in every conversation. Where they aren’t available, expect this behavior:- Claude blocks the origins, whatever the user allows
- Claude usually doesn’t ask the user to allow the origins
- The view opens without the site
Declare the origins that a view embeds
On your server, declare each origin that you put in an<iframe> in the view:
- Add one entry for each origin to
_meta.ui.csp.frameDomainson the view’sui://resource - Put
_metaon the content item that yourresources/readhandler returns. The quickstart’sresourceDomainsdeclaration is in the same place.
_meta object declares one origin:
_meta.ui fields csp, domain, and permissions together on the content item. If you set _meta.ui on the content item, Claude ignores _meta.ui on the resources/list entry.
Accepted forms for a frameDomains entry
Write each entry as an https origin in one of three forms:
- You can also add a port to a wildcard entry, such as
https://*.example.com:8443 - To embed
example.comitself, add a separatehttps://example.comentry. A wildcard entry doesn’t match that host. - Omit the path and the trailing slash
- Write an internationalized host name in its
xn--form
Hosts that Claude refuses in frameDomains
Claude refuses some hosts in any form. Examples are:
- Anthropic’s own domains
- IP addresses
- Private-network names, such as
app.internal - Wildcards over a suffix that unrelated parties share, such as
https://*.co.uk
Wildcard entries in a directory connector
For a directory connector, compare your wildcard entry with the origins that the listing allows:- The listing has the same wildcard or a wider one: your entry matches all the hosts below the domain
- The listing is narrower: your entry matches the listed origins that the wildcard includes
Read the allowed origins in the view
To see which origins Claude allowed for a view, readapp.getHostCapabilities()?.sandbox?.csp?.frameDomains in the view’s script after app.connect() resolves.
Choose what the view shows from the list that you read:
- The list is empty, or it doesn’t cover your site’s origin: show a fallback, such as a button that sends
ui/open-link - The list is missing: Claude hasn’t reported what it allowed. Load the
<iframe>, and keep the fallback on screen.
Allow Claude to frame your site
If your site restricts which pages can frame it, allow two origins in its response header:https://claude.ai and your connector’s sandbox host.
The sandbox host serves the view and is a subdomain of claudemcpcontent.com.
- On your server, set
ui.domainon the view’sui://resource. Withoutui.domain, the sandbox host differs between conversations. To compute the value, see Setui.domainfor Claude. - On your site, add both origins to the
frame-ancestorsdirective of theContent-Security-Policyheader
c3d80a4ed901ee05b21755a88273b4a4.claudemcpcontent.com with your connector’s own sandbox host:
frameDomains list that Claude reports to the view covers the site’s origin.
Browsers test frame-ancestors against every frame above the site, so the site doesn’t load if either entry is missing.
Test embedded sites with a custom connector
To try a site before your connector has a listing, add your server to Claude as a custom connector. For custom connectors, embedded sites are available to a limited set of organizations, and not in every conversation.- Declare the origin
- Allow Claude to frame your site. Compute the sandbox host for the address that you test with, because the host derives from the server URL.
- Add your server to Claude as a custom connector
- In Claude on the web, start a conversation with the unified Claude experience, which combines chat and Cowork. If the message box offers a choice of Chat or Cowork, your account doesn’t have it.
- Ask Claude to open the view, and name your connector in the message
- Allow the origins. Claude lists them in one of these places:
- In the approval request for the tool that opens the view.
- In a review that you open from a line beside the view. If the permission setting in the message box is Auto, Claude can open the view without asking, so use the review.
- Confirm that the site renders in the view
app.getHostCapabilities()?.sandbox?.csp?.frameDomains. A view that opened before you allowed them receives an empty list.
On Team and Enterprise plans, an Owner or Primary Owner controls the unified Claude experience with the Chat and Cowork unified setting in Organization settings > Capabilities.
Check whether your organization has access to embedded sites
To see whether embedded sites are available for custom connectors where the view opened, read the console line that namesframeDomains.
After you follow the test steps, your organization may not have access if all of these are true:
- Your connector counts as custom
- Claude didn’t ask about the origins, in the approval request or beside the view
- The view opens without the site
Limits on origins in a custom connector
- All or none: the view receives every origin that you declare in an accepted form, or none of them
- Repeated requests: expect Claude to ask again, for example in a new conversation, or when you declare an origin that the user hasn’t seen listed. Keep the list of origins short and stable.
- Claude’s own check of the origins: if the origins don’t pass the check, the view doesn’t open, and Claude shows a message in its place. If a single origin is refused, the whole view doesn’t open. Delete that origin from
frameDomains.
Design your site for the iframe sandbox
An embedded site inherits the sandbox of the view around it, and the browser treats the site as a third-party frame.- Pop-ups and new tabs:
window.open()andtarget="_blank"links don’t open. To open a link:- Post a message from the site to the view.
- In the view’s script, verify
event.originbefore you sendui/open-link.
- Dialogs:
alert(),prompt(), andprint()have no effect.confirm()returnsfalse. Build dialogs into the page. - Downloads: a link to a file doesn’t download the file
- Navigation: the site can navigate its own frame and submit forms to its own origin. The site can’t navigate the Claude page.
- Redirects: for a redirect in the frame, the destination and each origin that the redirect passes through must be origins that Claude allows
Sessions and cookies in an embedded site
Claude passes no session to the site, and a call todocument.requestStorageAccess() is rejected. To keep a session inside the frame:
- Set
ui.domainon the view’sui://resource, so that the site can uselocalStoragein the frame, and cookies in browsers that allow cookies in embedded sites - Set the session cookie’s attributes to
SameSite=None; Secure; Partitioned - Don’t depend on a session from another tab
Sign a user in to an embedded site
To sign a user in, pass a token from your server to the site through the view:- On your server, add a tool that returns a short-lived, single-use token
- Set the tool’s
_meta.ui.visibilityto["app"], so that Claude can’t call the tool - In the view’s script, call the tool with
app.callServerTool() - From the view, send the token to the frame with
postMessage, and set the target origin to the site’s exact origin - On the site, exchange the token for a session
Device features in views that embed sites
A view can’t use the camera, the microphone, location, or clipboard write when Claude allows an origin for it. The sites that the view embeds can’t use them either. The view loses these features even if you request them in_meta.ui.permissions.
Plan for other views to lose the same features, including views that embed nothing:
- Directory connector: when the listing allows an origin, plan for the connector’s other views to lose them
- Custom connector: where embedded sites are available for custom connectors, plan for every view of the connector to lose them
app.getHostCapabilities()?.sandbox?.permissions in the view’s script after app.connect() resolves. Each key is a granted feature.
Also plan for Element.requestFullscreen() to fail, in views and in embedded sites. Don’t depend on a video player’s full-screen button.
Debug an embedded site that fails to load
When a view opens without its site, open the browser’s developer console on the Claude page and match the message to its cause. In a Claude app, open the console from the developer tools.Content Security Policy error names frame-src
Claude didn’t allow the origin for this view, and your site receives no request. Check each of these:
- The entry has an accepted form
- The
_metaobject is on the content item - Claude allows origins for your kind of connector
Content Security Policy error names frame-ancestors
Your site’s header doesn’t include the sandbox host or https://claude.ai, or the view has no fixed sandbox host. Follow the steps in Allow Claude to frame your site.
Console error names X-Frame-Options
Your site sends DENY or SAMEORIGIN in X-Frame-Options, and it sends no frame-ancestors directive. Send a frame-ancestors directive that lists the sandbox host and https://claude.ai. When a browser receives both headers, it obeys frame-ancestors.
Console line names frameDomains
Look for a console line that begins with [mcp-ext-apps-host] and names frameDomains. In that line, Claude reports what it did with the origins that you declared. If the console has no such line, don’t conclude that Claude allowed the origin.
For a custom connector, use the wording of the line to identify your case:
was opened without the sites its resource declares: embedded sites are available for the connector in that conversation. Either the origins weren’t allowed, or they didn’t pass Claude’s own check.Not among the sites that Claude allows this connector to show: embedded sites aren’t available for custom connectors where the view opened, or the connector doesn’t count as custom
Unrecognized Content-Security-Policy directive 'webrtc'., which come from Claude’s own frames.
Next steps
- Show your MCP App in the side panel or full screen: give the embedded site more room than the conversation offers
- Design guidelines: declare the other
_meta.ui.cspfields, for the requests and assets that the view itself loads