Max Narditdata & ai systems
← Articles
12 mindev

MCP Apps in Claude: app tools, entry points and embedded sites, and which of them your users actually get

Three new MCP Apps features in Claude, each behind its own set of gates. The connector you test with clears more of them than the install your users get from a Community listing, and per the docs the user sees no message when a feature is withheld.

Contents

On November 10 Anthropic announced that Claude now supports more of the MCP Apps extension: an app can open beside the conversation or full screen, Claude can call tools that live in the app’s interface, and an app can embed the vendor’s own website. Adobe, Atlassian and Box were among the launch examples. For developers the post calls the new capabilities a public preview. The extension’s stable spec is still 2026-01-26; what changed is how much of it Claude implements.

If you haven’t met MCP Apps yet: a tool on your MCP server points at a ui:// resource, and the host renders that HTML, on the web in a sandboxed iframe, next to the text result. I covered the basics in the Claude plugin directory explainer. This piece is about the three new features with conditions attached, and one sentence on the getting-started page that is easy to read past.

All three need a remote server. Entry points and embedded sites also depend on how your connector is listed in the directory, and app tools and entry points (plus embedded sites on a custom connector) are limited to some organizations for now. The connector you test with, added by URL, clears more of those gates than the install your users get from a Community listing, and there is no application for the Verified label that would close the gap. So I’d design for the Community install first and treat each new feature as an upgrade the app detects at runtime.

Everything below is from Anthropic’s documentation, read on November 11, plus commands I ran locally. I haven’t put an MCP App in front of Claude myself.

What did Claude add to MCP Apps on November 10?

The getting-started page lists six capabilities: side panel and full screen, entry points from the Artifacts page, app tools, messages from the app to Claude, embedded websites, and downloads. This piece covers three of them.

App tools let the view (one rendered instance of your app) register tools that Claude can call to change what is on screen. The docs’ example is a shopping list: you ask Claude to add milk, and milk appears in the list. Entry points let people open your app from the Artifacts page without asking Claude for it, and pin it to the sidebar. Embedded sites load your own website in an iframe inside the view, so your product’s editor or dashboard can run in Claude. The side panel has an organization check of its own; the getting-started page names it next to entry points and app tools.

The announcement says public preview; the docs say app tools and entry points, and embedded sites on custom connectors, are available to “a limited set of organizations”. I read those as the same thing from two angles: anyone can build, not every account will see it yet.

Which MCP Apps features in Claude need a remote server or a Verified listing?

All three need a remote server; only entry points and embedded sites care about the directory listing. The docs spread this over four pages, so here it is in one place:

FeatureConnector typeDirectory listingOther gates
App toolsRemote onlyNo listing conditionLimited set of organizations; unified Claude experience; test on the web
Entry pointsRemote onlyVerified or custom connector. Community installs get none, pinned apps includedLimited set of organizations; unified Claude experience
Embedded sitesRemote onlyVerified: only origins the listing allows. Community: blockedCustom connector: the user must allow the origin, only in a limited set of organizations and not in every conversation

“Unified Claude experience” is the new combined chat and Cowork interface. Per the help article, an account whose message box still offers a choice of Chat or Cowork doesn’t have it, and on Team and Enterprise an owner switches it on under Organization settings, Capabilities.

A local server, one in claude_desktop_config.json, gets none of the three, and the docs say so on each feature’s page. They also point out that the example servers they hand you run locally. By those rules the demos can show you the inline view and nothing from this table.

Why does an MCP App work in a Claude test and fail for users who install it from the directory?

Because the connector you test with and the one your users install are different kinds of connector, even when they point at the same server. You add yours by URL, so Claude treats it as a custom connector. A user who installs your Community listing gets a directory connector, and the getting-started page says what that costs:

People who install your connector from a Community listing don’t get either
one, even if both worked while you tested with a custom connector.

“Either one” means entry points and embedded sites. The entry-points page describes the exact case: you add your server by URL, the same server also has a Community listing, and you keep the entry point while everyone who installed the listing has none.

What counts as custom is decided per feature, and the embedded-sites page is stricter than the entry-points one:

  • For embedded sites, a by-URL connector counts as custom only if its address matches no directory listing. Point your test connector at your production URL after you list it, and Claude treats it as the directory connector: a site that loaded while you tested is blocked under a Community listing.
  • Claude ignores the query string when it matches an address to a listing, so ?build=next on the same path is still the listed connector. Serve a test build at its own address.
  • A path with a capital letter, %, ; or // doesn’t count as custom, and Claude blocks its origins. A connector added many months ago may not count as custom either; the docs’ fix is to remove it and add it again.

The user gets no message in either case. Per the docs, an entry point that doesn’t qualify is not listed on the Artifacts page, with no error, and a blocked embedded site leaves the iframe empty: “Claude shows no message”. The app can find out, though, which is what the fallback further down relies on.

How does a Claude connector get Verified?

You can’t apply for it. Per the connector verification page, a submitted server is scanned automatically and listed as Community by default. Anthropic may then escalate listings it flags as highly useful to a Verified review, in which a person tests each tool:

That escalation is assessed automatically. There’s no separate application
for the Verified label, and you don’t need to take any action.

Since nobody can apply, plan as if Community is where your listing stays. If your app is only useful with your embedded editor inside it, a Community listing ships an app that isn’t useful. If the inline view works on its own and the embedded site is an upgrade, Community users get a smaller product, not a broken one.

Verified doesn’t settle it for good either. The listing has to allow each embedded origin, so a new origin you declare can’t load until the listing allows it as well.

How do you compute ui.domain for an MCP App in Claude?

Take the first 32 hex characters of the SHA-256 of your server URL and append .claudemcpcontent.com. Declare that value as _meta.ui.domain on the view’s resource when the app runs its own OAuth flow, when your site has to allow Claude to frame it, or when the embedded site should keep localStorage between loads. Without it the sandbox host changes between conversations. The docs give a Node one-liner:

shell
node -e 'const yourServerUrl = "https://example.com/mcp"; console.log(require("crypto").createHash("sha256").update(yourServerUrl).digest("hex").slice(0,32) + ".claudemcpcontent.com")'

On Node 22.23.4 the docs’ example reproduces (c3d80a4ed901ee05b21755a88273b4a4.claudemcpcontent.com). For this site’s own read-only MCP server, the two spellings of the URL give two different values:

https://max.nardit.com/mcp    2e8c4b9cbca6508f73a038113ef1c06b.claudemcpcontent.com
https://max.nardit.com/mcp/   922f395950b978b8d99c303db3fe7e4f.claudemcpcontent.com

The troubleshooting page warns about exactly this: the hash covers the full string, scheme, path and trailing slash included, so hash the URL configured under Customize, Connectors. A mismatch shows ui.domain mismatch instead of the app. A local stdio server has no URL to hash, so ui.domain isn’t available for it at all.

Put csp, domain and permissions together under _meta.ui on the content item your resources/read handler returns. If _meta.ui is set there, Claude ignores the copy on the resources/list entry.

How do you embed your own website in a Claude MCP App?

Per the embedded-sites page: declare the origin on the view, allow Claude to frame the site, and build a fallback for when Claude doesn’t load it. The fallback is the step a Community install depends on.

Declare. Add each origin to _meta.ui.csp.frameDomains on the view’s ui:// resource:

json
{
  "_meta": {
    "ui": {
      "csp": {
        "frameDomains": ["https://editor.example.com"]
      }
    }
  }
}

Entries are https origins: exact, with a port, or a subdomain wildcard. A wildcard doesn’t cover the bare domain, and an entry with a path or trailing slash is treated as undeclared. Claude refuses its own domains, IP addresses, private-network names and wildcards over a shared suffix such as *.co.uk.

Allow framing. If your site restricts framing, its frame-ancestors must list both https://claude.ai and your exact sandbox host. Browsers check every frame above the site, so one missing entry blocks it:

Content-Security-Policy: frame-ancestors https://<your-hash>.claudemcpcontent.com https://claude.ai

The docs warn against the shortcut: a wildcard over claudemcpcontent.com lets any other connector’s MCP App frame your site.

Fall back. After app.connect() resolves, read the allowed list and decide what to show:

ts
const allowed = app.getHostCapabilities()?.sandbox?.csp?.frameDomains;

An empty list, or one that doesn’t cover your origin, means Claude blocked it: show a button that opens the site in a new tab through ui/open-link. That is the branch a Community install lands in. A missing list means Claude hasn’t reported either way, so load the iframe and keep the button on screen.

Sign in. Claude passes no session to the embedded site, a login in another tab doesn’t count, and document.requestStorageAccess() is rejected. The documented route: a server tool, hidden from Claude with _meta.ui.visibility set to app only, that returns a short-lived single-use token; the view fetches it with app.callServerTool() and posts it to the frame at its exact origin; the site exchanges it for a session cookie set with SameSite=None; Secure; Partitioned.

Two costs come with an allowed origin. A view with one loses camera, microphone, location and clipboard write, and so do the sites it embeds, even if you ask for them in _meta.ui.permissions. Plan for the connector’s other views to lose them too, for a directory connector once the listing allows an origin and for a custom one wherever embedded sites are available. And the iframe sandbox blocks pop-ups, alert() and file downloads, while confirm() returns false.

How do you declare an MCP App entry point for Claude?

Mark the tool that opens the view: keep ui.resourceUri in its _meta and add an openai/ui key whose entrypoints list has a global entry. The entry-points page shows it as it appears in tools/list:

json
{
  "name": "open_studio",
  "title": "Example Studio",
  "description": "Open Example Studio.",
  "inputSchema": { "type": "object" },
  "_meta": {
    "ui": { "resourceUri": "ui://example/home" },
    "openai/ui": { "entrypoints": [{ "type": "global" }] }
  }
}

The key comes from OpenAI’s MCP extensions, so a tool built for the OpenAI Apps SDK may already carry it; the docs’ first check for such a tool is that ui.resourceUri is there too. The view opened this way starts with no request and no conversation, so every input has to be optional and the empty arguments object has to produce a useful screen. Keep _meta.ui.visibility unset or including model. And watch the tool name: two underscores or two dots in a row, or a dot next to an underscore, gets the app listed but impossible to open. After a change, refresh the tools list under Customize, Connectors, or wait up to about two hours.

How do you register an MCP App tool that Claude can call?

In the view’s script, with app.registerTool, before app.connect(). The SDK has two functions with nearly the same name: registerAppTool runs on your server and registers the tool that opens the view; app.registerTool runs in the view and registers a tool Claude can call while the view is open.

Most of what the app-tools page warns about fails for the whole view, not for one tool:

  • A plain JSON Schema object passed to app.registerTool makes the view’s tools/list fail, which hides every tool in it. The method wants a schema object from a library that implements Standard Schema with JSON Schema output, such as zod 4.2 or later, and you also need @modelcontextprotocol/ext-apps 1.7.0 or later (published April 21; 2.0.4 is current as of November 11).
  • An input schema without type: "object" at the root, more than 64 tools, or more than 1 MiB of definitions, and Claude can use none of the view’s tools.

To write JSON Schema yourself, or with the SDK loaded from a CDN, use the onlisttools and oncalltool handlers instead, pass { tools: { listChanged: true } } as the second argument to the App constructor, and validate the arguments, because the handlers don’t.

A call has 45 seconds, approval time not counted; for longer work, start it, return, and register a second tool that reports the outcome. Claude lists each tool by its name and the first line of its description, up to 200 characters, so make that line complete.

Approval follows the tool definition. The prompt names the connector and the tool’s title, and Always allow is saved for the tool as defined, so changing its name, title, description or schema can bring the prompt back. Keep definitions constant and put changing values in results.

One asymmetry the docs mention in passing: when your view calls your own server’s tools with app.callServerTool, the user sees no prompt at all. In my WebMCP piece I called a tool list an interface, not a boundary; app tools are the same idea inside Claude. The prompt covers what Claude asks the view to do. What the view then does with your server is your code’s decision, and authorization for it still belongs on the server.

How do you test an MCP App for Claude before you ship it?

Before the listing exists you can only simulate the install your users get, so simulate it on purpose. A plan built from the docs:

  1. Serve the server remotely (a tunnelled local server counts as custom), at a lower-case path, with its own address per build.
  2. Add it as a custom connector and test on the web in a conversation with the unified experience, permission setting on Manual so you see each approval.
  3. Open the app in one message and ask for an app-tool action in the next. Claude receives a new view’s tools with your following message.
  4. If app tools, entry points or embedded sites don’t appear and nothing else explains it, your organization may not be in the rollout. The docs’ advice is to keep the app working without them and test later.
  5. Run the Community case deliberately: force the empty-frameDomains branch, remove the entry point, and check that what’s left is still worth installing.
  6. After the listing is live, point a by-URL connector at the listed address. By the embedded-sites rule above it stops counting as custom, so you see what a directory install sees.

For debugging, the SDK logs its protocol messages with console.debug, so in Chrome you need the Verbose level. For embedded sites, the troubleshooting section says to look for a console line starting with [mcp-ext-apps-host] that names frameDomains: that is where Claude reports what it did with your origins.

What the November MCP Apps update changes for a server author

Reading the docs, the gates are what I’d design around, more than any one API. As documented, the inline view has no listing condition; the new features do. One app now has several shapes, picked per user by gates you don’t control, and the user isn’t told which shape they got. Build the smallest shape first: the inline view, no app tools, no embedded site, working from a local server. Add each feature as an upgrade that checks for itself, with frameDomains read back, app tools optional and a useful empty state for entry points.

This site runs a small remote MCP server at /mcp: read-only tools, no sign-in, no UI resource, in place since October 8. If I gave it a view, the table says it would clear the remote-server condition for all three features, and a user who added it as https://max.nardit.com/mcp would need the 2e8c4b9c… value computed above. That’s about all the server could know. Which shape of the app a given user gets depends on their organization, their interface and how they installed the connector, and none of that reaches the server.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles